Compare commits
25 Commits
testing-qb
...
v0.87.0-cf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cffeff0d2a | ||
|
|
fc592776a5 | ||
|
|
af2534bbf5 | ||
|
|
1ee1ca7951 | ||
|
|
3b1bf34d3e | ||
|
|
fbcff29fae | ||
|
|
81fcca3bd3 | ||
|
|
4f7d84aa37 | ||
|
|
8f8dedb8b3 | ||
|
|
3f65229506 | ||
|
|
f006260719 | ||
|
|
3fc8f6c353 | ||
|
|
e02ae9a5c4 | ||
|
|
1989d07e52 | ||
|
|
78194ae955 | ||
|
|
da1b6d1ed0 | ||
|
|
d3c76ae8be | ||
|
|
bed3dbc698 | ||
|
|
66affb0ece | ||
|
|
75f62372ae | ||
|
|
a3ac307b4e | ||
|
|
7672d2f636 | ||
|
|
e3018d9529 | ||
|
|
385ee268e3 | ||
|
|
01036a8a2f |
@@ -224,3 +224,6 @@ statsreporter:
|
||||
enabled: true
|
||||
# The interval at which the stats are collected.
|
||||
interval: 6h
|
||||
collect:
|
||||
# Whether to collect identities and traits (emails).
|
||||
identities: true
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/jmoiron/sqlx"
|
||||
|
||||
"github.com/SigNoz/signoz/ee/query-service/app/api"
|
||||
"github.com/SigNoz/signoz/ee/query-service/app/db"
|
||||
@@ -107,7 +106,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
)
|
||||
|
||||
rm, err := makeRulesManager(
|
||||
serverOptions.SigNoz.SQLStore.SQLxDB(),
|
||||
reader,
|
||||
serverOptions.SigNoz.Cache,
|
||||
serverOptions.SigNoz.Alertmanager,
|
||||
@@ -444,7 +442,6 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func makeRulesManager(
|
||||
db *sqlx.DB,
|
||||
ch baseint.Reader,
|
||||
cache cache.Cache,
|
||||
alertmanager alertmanager.Alertmanager,
|
||||
@@ -457,7 +454,6 @@ func makeRulesManager(
|
||||
managerOpts := &baserules.ManagerOptions{
|
||||
TelemetryStore: telemetryStore,
|
||||
Prometheus: prometheus,
|
||||
DBConn: db,
|
||||
Context: context.Background(),
|
||||
Logger: zap.L(),
|
||||
Reader: ch,
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/pgdialect"
|
||||
)
|
||||
@@ -19,7 +18,6 @@ type provider struct {
|
||||
settings factory.ScopedProviderSettings
|
||||
sqldb *sql.DB
|
||||
bundb *sqlstore.BunDB
|
||||
sqlxdb *sqlx.DB
|
||||
dialect *dialect
|
||||
}
|
||||
|
||||
@@ -61,7 +59,6 @@ func New(ctx context.Context, providerSettings factory.ProviderSettings, config
|
||||
settings: settings,
|
||||
sqldb: sqldb,
|
||||
bundb: sqlstore.NewBunDB(settings, sqldb, pgdialect.New(), hooks),
|
||||
sqlxdb: sqlx.NewDb(sqldb, "postgres"),
|
||||
dialect: new(dialect),
|
||||
}, nil
|
||||
}
|
||||
@@ -74,10 +71,6 @@ func (provider *provider) SQLDB() *sql.DB {
|
||||
return provider.sqldb
|
||||
}
|
||||
|
||||
func (provider *provider) SQLxDB() *sqlx.DB {
|
||||
return provider.sqlxdb
|
||||
}
|
||||
|
||||
func (provider *provider) Dialect() sqlstore.SQLDialect {
|
||||
return provider.dialect
|
||||
}
|
||||
|
||||
@@ -129,5 +129,6 @@
|
||||
"text_num_points": "data points in each result group",
|
||||
"text_alert_frequency": "Run alert every",
|
||||
"text_for": "minutes",
|
||||
"selected_query_placeholder": "Select query"
|
||||
"selected_query_placeholder": "Select query",
|
||||
"alert_rule_not_found": "Alert Rule not found"
|
||||
}
|
||||
|
||||
@@ -126,7 +126,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
|
||||
const isRouteEnabledForWorkspaceBlockedState =
|
||||
isAdmin &&
|
||||
(path === ROUTES.ORG_SETTINGS ||
|
||||
(path === ROUTES.SETTINGS ||
|
||||
path === ROUTES.ORG_SETTINGS ||
|
||||
path === ROUTES.BILLING ||
|
||||
path === ROUTES.MY_SETTINGS);
|
||||
|
||||
|
||||
@@ -131,10 +131,6 @@ export const CreateAlertChannelAlerts = Loadable(
|
||||
() => import(/* webpackChunkName: "Create Channels" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const EditAlertChannelsAlerts = Loadable(
|
||||
() => import(/* webpackChunkName: "Edit Channels" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
export const AllAlertChannels = Loadable(
|
||||
() => import(/* webpackChunkName: "All Channels" */ 'pages/Settings'),
|
||||
);
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
CreateNewAlerts,
|
||||
DashboardPage,
|
||||
DashboardWidget,
|
||||
EditAlertChannelsAlerts,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
Home,
|
||||
@@ -253,13 +252,6 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'CHANNELS_NEW',
|
||||
},
|
||||
{
|
||||
path: ROUTES.CHANNELS_EDIT,
|
||||
exact: true,
|
||||
component: EditAlertChannelsAlerts,
|
||||
isPrivate: true,
|
||||
key: 'CHANNELS_EDIT',
|
||||
},
|
||||
{
|
||||
path: ROUTES.ALL_CHANNELS,
|
||||
exact: true,
|
||||
|
||||
29
frontend/src/api/changelog/getChangelogByVersion.ts
Normal file
29
frontend/src/api/changelog/getChangelogByVersion.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||
|
||||
const getChangelogByVersion = async (
|
||||
versionId: string,
|
||||
): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> => {
|
||||
try {
|
||||
const response = await axios.get(`
|
||||
https://cms.signoz.cloud/api/release-changelogs?filters[version][$eq]=${versionId}&populate[features][sort]=sort_order:asc&populate[features][populate][media][fields]=id,ext,url,mime,alternativeText
|
||||
`);
|
||||
|
||||
if (!Array.isArray(response.data.data) || response.data.data.length === 0) {
|
||||
throw new Error('No changelog found!');
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: response.statusText,
|
||||
payload: response.data.data[0],
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default getChangelogByVersion;
|
||||
@@ -119,6 +119,7 @@ export const updateFunnelSteps = async (
|
||||
export interface ValidateFunnelPayload {
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface ValidateFunnelResponse {
|
||||
@@ -132,12 +133,11 @@ export interface ValidateFunnelResponse {
|
||||
}
|
||||
|
||||
export const validateFunnelSteps = async (
|
||||
funnelId: string,
|
||||
payload: ValidateFunnelPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<ValidateFunnelResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/validate`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/validate`,
|
||||
payload,
|
||||
{ signal },
|
||||
);
|
||||
@@ -185,6 +185,7 @@ export interface FunnelOverviewPayload {
|
||||
end_time: number;
|
||||
step_start?: number;
|
||||
step_end?: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface FunnelOverviewResponse {
|
||||
@@ -202,12 +203,11 @@ export interface FunnelOverviewResponse {
|
||||
}
|
||||
|
||||
export const getFunnelOverview = async (
|
||||
funnelId: string,
|
||||
payload: FunnelOverviewPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<FunnelOverviewResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/overview`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/overview`,
|
||||
payload,
|
||||
{
|
||||
signal,
|
||||
@@ -235,12 +235,11 @@ export interface SlowTraceData {
|
||||
}
|
||||
|
||||
export const getFunnelSlowTraces = async (
|
||||
funnelId: string,
|
||||
payload: FunnelOverviewPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<SlowTraceData> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/slow-traces`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/slow-traces`,
|
||||
payload,
|
||||
{
|
||||
signal,
|
||||
@@ -273,7 +272,7 @@ export const getFunnelErrorTraces = async (
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<ErrorTraceData> | ErrorResponse> => {
|
||||
const response: AxiosResponse = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/error-traces`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/error-traces`,
|
||||
payload,
|
||||
{
|
||||
signal,
|
||||
@@ -291,6 +290,7 @@ export const getFunnelErrorTraces = async (
|
||||
export interface FunnelStepsPayload {
|
||||
start_time: number;
|
||||
end_time: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface FunnelStepGraphMetrics {
|
||||
@@ -307,12 +307,11 @@ export interface FunnelStepsResponse {
|
||||
}
|
||||
|
||||
export const getFunnelSteps = async (
|
||||
funnelId: string,
|
||||
payload: FunnelStepsPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<FunnelStepsResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/steps`,
|
||||
payload,
|
||||
{ signal },
|
||||
);
|
||||
@@ -330,6 +329,7 @@ export interface FunnelStepsOverviewPayload {
|
||||
end_time: number;
|
||||
step_start?: number;
|
||||
step_end?: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
export interface FunnelStepsOverviewResponse {
|
||||
@@ -341,12 +341,11 @@ export interface FunnelStepsOverviewResponse {
|
||||
}
|
||||
|
||||
export const getFunnelStepsOverview = async (
|
||||
funnelId: string,
|
||||
payload: FunnelStepsOverviewPayload,
|
||||
signal?: AbortSignal,
|
||||
): Promise<SuccessResponse<FunnelStepsOverviewResponse> | ErrorResponse> => {
|
||||
const response = await axios.post(
|
||||
`${FUNNELS_BASE_PATH}/${funnelId}/analytics/steps/overview`,
|
||||
`${FUNNELS_BASE_PATH}/analytics/steps/overview`,
|
||||
payload,
|
||||
{ signal },
|
||||
);
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
.changelog-modal {
|
||||
.ant-modal-content {
|
||||
padding: unset;
|
||||
background-color: var(--bg-ink-400, #121317);
|
||||
|
||||
.ant-modal-header {
|
||||
margin-bottom: unset;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
margin-top: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background-color: var(--bg-ink-400, #121317);
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-vanilla-100, #fff);
|
||||
border-bottom: 1px solid var(--bg-slate-500, #161922);
|
||||
}
|
||||
|
||||
&-footer.scroll-available {
|
||||
.scroll-btn-container {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
position: relative;
|
||||
border: 1px solid var(--bg-slate-500, #161922);
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&-label {
|
||||
color: var(--text-robin-400, #7190f9);
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
position: relative;
|
||||
padding-left: 14px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
background-color: var(--bg-robin-500, #7190f9);
|
||||
}
|
||||
}
|
||||
|
||||
&-ctas {
|
||||
display: flex;
|
||||
|
||||
& svg {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-btn-container {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.scroll-btn {
|
||||
all: unset;
|
||||
padding: 4px 12px 4px 10px;
|
||||
background-color: var(--bg-slate-400, #1d212d);
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
transition: background-color 0.1s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg-slate-200, #2c3140);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--bg-slate-600, #1c1f2a);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: var(--text-vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
// add animation to the chevrons down icon
|
||||
svg {
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
max-height: calc(100vh - 300px);
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--bg-slate-500, #161922);
|
||||
border-top-width: 0;
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// pulse for the scroll for more icon
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.changelog-modal {
|
||||
.ant-modal-content {
|
||||
background-color: var(--bg-vanilla-100);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&-title {
|
||||
background: var(--bg-vanilla-100);
|
||||
color: var(--bg-ink-500);
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&-content {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
}
|
||||
|
||||
&-footer {
|
||||
border-color: var(--bg-vanilla-300);
|
||||
|
||||
.scroll-btn-container {
|
||||
.scroll-btn {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
|
||||
span {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
131
frontend/src/components/ChangelogModal/ChangelogModal.tsx
Normal file
131
frontend/src/components/ChangelogModal/ChangelogModal.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import './ChangelogModal.styles.scss';
|
||||
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Modal } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import dayjs from 'dayjs';
|
||||
import { ChevronsDown, ScrollText } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import ChangelogRenderer from './components/ChangelogRenderer';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function ChangelogModal({ onClose }: Props): JSX.Element {
|
||||
const [hasScroll, setHasScroll] = useState(false);
|
||||
const changelogContentSectionRef = useRef<HTMLDivElement>(null);
|
||||
const { changelog } = useAppContext();
|
||||
|
||||
const formattedReleaseDate = dayjs(changelog?.release_date).format(
|
||||
'MMMM D, YYYY',
|
||||
);
|
||||
|
||||
const checkScroll = useCallback((): void => {
|
||||
if (changelogContentSectionRef.current) {
|
||||
const {
|
||||
scrollHeight,
|
||||
clientHeight,
|
||||
scrollTop,
|
||||
} = changelogContentSectionRef.current;
|
||||
const isAtBottom = scrollHeight - clientHeight - scrollTop <= 8;
|
||||
setHasScroll(scrollHeight > clientHeight + 24 && !isAtBottom); // 24px - buffer height to show show more
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkScroll();
|
||||
const changelogContentSection = changelogContentSectionRef.current;
|
||||
|
||||
if (changelogContentSection) {
|
||||
changelogContentSection.addEventListener('scroll', checkScroll);
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
if (changelogContentSection) {
|
||||
changelogContentSection.removeEventListener('scroll', checkScroll);
|
||||
}
|
||||
};
|
||||
}, [checkScroll]);
|
||||
|
||||
const onClickUpdateWorkspace = (): void => {
|
||||
window.open(
|
||||
'https://github.com/SigNoz/signoz/releases',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
};
|
||||
|
||||
const onClickScrollForMore = (): void => {
|
||||
if (changelogContentSectionRef.current) {
|
||||
changelogContentSectionRef.current.scrollTo({
|
||||
top: changelogContentSectionRef.current.scrollTop + 600, // Scroll 600px from the current position
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={cx('changelog-modal')}
|
||||
title={
|
||||
<div className="changelog-modal-title">
|
||||
<ScrollText size={16} />
|
||||
What’s New ⎯ Changelog : {formattedReleaseDate}
|
||||
</div>
|
||||
}
|
||||
width={820}
|
||||
open
|
||||
onCancel={onClose}
|
||||
footer={
|
||||
<div
|
||||
className={cx('changelog-modal-footer', hasScroll && 'scroll-available')}
|
||||
>
|
||||
{changelog?.features && changelog.features.length > 0 && (
|
||||
<span className="changelog-modal-footer-label">
|
||||
{changelog.features.length} new
|
||||
{changelog.features.length > 1 ? 'features' : 'feature'}
|
||||
</span>
|
||||
)}
|
||||
<div className="changelog-modal-footer-ctas">
|
||||
<Button type="default" icon={<CloseOutlined />} onClick={onClose}>
|
||||
Skip for now
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CheckOutlined />}
|
||||
onClick={onClickUpdateWorkspace}
|
||||
>
|
||||
Update my workspace
|
||||
</Button>
|
||||
</div>
|
||||
{changelog && (
|
||||
<div className="scroll-btn-container">
|
||||
<button
|
||||
data-testid="scroll-more-btn"
|
||||
type="button"
|
||||
className="scroll-btn"
|
||||
onClick={onClickScrollForMore}
|
||||
>
|
||||
<ChevronsDown size={14} />
|
||||
<span>Scroll for more</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="changelog-modal-content"
|
||||
data-testid="changelog-content"
|
||||
ref={changelogContentSectionRef}
|
||||
>
|
||||
{changelog && <ChangelogRenderer changelog={changelog} />}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangelogModal;
|
||||
@@ -0,0 +1,79 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import ChangelogModal from '../ChangelogModal';
|
||||
|
||||
const mockChangelog = {
|
||||
release_date: '2025-06-10',
|
||||
features: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Feature 1',
|
||||
description: 'Description for feature 1',
|
||||
media: null,
|
||||
},
|
||||
],
|
||||
bug_fixes: 'Bug fix details',
|
||||
maintenance: 'Maintenance details',
|
||||
};
|
||||
|
||||
// Mock react-markdown to just render children as plain text
|
||||
jest.mock(
|
||||
'react-markdown',
|
||||
() =>
|
||||
function ReactMarkdown({ children }: any) {
|
||||
return <div>{children}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
// mock useAppContext
|
||||
jest.mock('providers/App/App', () => ({
|
||||
useAppContext: jest.fn(() => ({ changelog: mockChangelog })),
|
||||
}));
|
||||
|
||||
describe('ChangelogModal', () => {
|
||||
it('renders modal with changelog data', () => {
|
||||
render(<ChangelogModal onClose={jest.fn()} />);
|
||||
expect(
|
||||
screen.getByText('What’s New ⎯ Changelog : June 10, 2025'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Feature 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Description for feature 1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Bug fix details')).toBeInTheDocument();
|
||||
expect(screen.getByText('Maintenance details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when Skip for now is clicked', () => {
|
||||
const onClose = jest.fn();
|
||||
render(<ChangelogModal onClose={onClose} />);
|
||||
fireEvent.click(screen.getByText('Skip for now'));
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens migration docs when Update my workspace is clicked', () => {
|
||||
window.open = jest.fn();
|
||||
render(<ChangelogModal onClose={jest.fn()} />);
|
||||
fireEvent.click(screen.getByText('Update my workspace'));
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
'https://github.com/SigNoz/signoz/releases',
|
||||
'_blank',
|
||||
'noopener,noreferrer',
|
||||
);
|
||||
});
|
||||
|
||||
it('scrolls for more when Scroll for more is clicked', () => {
|
||||
render(<ChangelogModal onClose={jest.fn()} />);
|
||||
const scrollBtn = screen.getByTestId('scroll-more-btn');
|
||||
const contentDiv = screen.getByTestId('changelog-content');
|
||||
if (contentDiv) {
|
||||
contentDiv.scrollTo = jest.fn();
|
||||
}
|
||||
fireEvent.click(scrollBtn);
|
||||
if (contentDiv) {
|
||||
expect(contentDiv.scrollTo).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import ChangelogRenderer from '../components/ChangelogRenderer';
|
||||
|
||||
// Mock react-markdown to just render children as plain text
|
||||
jest.mock(
|
||||
'react-markdown',
|
||||
() =>
|
||||
function ReactMarkdown({ children }: any) {
|
||||
return <div>{children}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
const mockChangelog = {
|
||||
id: 1,
|
||||
documentId: 'changelog-doc-1',
|
||||
version: '1.0.0',
|
||||
createdAt: '2025-06-09T12:00:00Z',
|
||||
release_date: '2025-06-10',
|
||||
features: [
|
||||
{
|
||||
id: 1,
|
||||
documentId: '1',
|
||||
title: 'Feature 1',
|
||||
description: 'Description for feature 1',
|
||||
sort_order: 1,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
publishedAt: '',
|
||||
deployment_type: 'All',
|
||||
media: {
|
||||
id: 1,
|
||||
documentId: 'doc1',
|
||||
ext: '.webp',
|
||||
url: '/uploads/feature1.webp',
|
||||
mime: 'image/webp',
|
||||
alternativeText: null,
|
||||
},
|
||||
},
|
||||
],
|
||||
bug_fixes: 'Bug fix details',
|
||||
updatedAt: '2025-06-09T12:00:00Z',
|
||||
publishedAt: '2025-06-09T12:00:00Z',
|
||||
maintenance: 'Maintenance details',
|
||||
};
|
||||
|
||||
describe('ChangelogRenderer', () => {
|
||||
it('renders release date', () => {
|
||||
render(<ChangelogRenderer changelog={mockChangelog} />);
|
||||
expect(screen.getByText('June 10, 2025')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders features, media, and description', () => {
|
||||
render(<ChangelogRenderer changelog={mockChangelog} />);
|
||||
expect(screen.getByText('Feature 1')).toBeInTheDocument();
|
||||
expect(screen.getByAltText('Media')).toBeInTheDocument();
|
||||
expect(screen.getByText('Description for feature 1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
.changelog-renderer {
|
||||
position: relative;
|
||||
padding-left: 20px;
|
||||
|
||||
.changelog-release-date {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
&-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
&-line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
bottom: -30px;
|
||||
width: 1px;
|
||||
background-color: var(--bg-slate-400, #1d212d);
|
||||
|
||||
.inner-ball {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 100%;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--bg-robin-500, #7190f9);
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding-left: 30px;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
top: 10px;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background-color: var(--bg-robin-500, #7190f9);
|
||||
transform: translate(-100%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li,
|
||||
p {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-vanilla-400, #c0c1c3);
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 2px 4px;
|
||||
background-color: var(--bg-slate-500, #161922);
|
||||
border-radius: 6px;
|
||||
font-size: 95%;
|
||||
vertical-align: middle;
|
||||
border: 1px solid var(--bg-slate-600, #1c1f2a);
|
||||
}
|
||||
a {
|
||||
color: var(--text-robin-500, #7190f9);
|
||||
font-weight: 600;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
color: var(--text-vanilla-100, #fff);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.changelog-media-image {
|
||||
height: auto;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.changelog-renderer {
|
||||
.changelog-release-date {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
&-line {
|
||||
background-color: var(--bg-vanilla-300);
|
||||
}
|
||||
li,
|
||||
p {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: var(--text-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import './ChangelogRenderer.styles.scss';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import {
|
||||
ChangelogSchema,
|
||||
Media,
|
||||
SupportedImageTypes,
|
||||
SupportedVideoTypes,
|
||||
} from 'types/api/changelog/getChangelogByVersion';
|
||||
|
||||
interface Props {
|
||||
changelog: ChangelogSchema;
|
||||
}
|
||||
|
||||
function renderMedia(media: Media): JSX.Element | null {
|
||||
if (SupportedImageTypes.includes(media.ext)) {
|
||||
return (
|
||||
<img
|
||||
src={media.url}
|
||||
alt={media.alternativeText || 'Media'}
|
||||
width={800}
|
||||
height={450}
|
||||
className="changelog-media-image"
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (SupportedVideoTypes.includes(media.ext)) {
|
||||
return (
|
||||
<video
|
||||
autoPlay
|
||||
controls
|
||||
controlsList="nodownload noplaybackrate"
|
||||
loop
|
||||
className="my-3 h-auto w-full rounded"
|
||||
>
|
||||
<source src={media.url} type={media.mime} />
|
||||
<track kind="captions" src="" label="No captions available" default />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function ChangelogRenderer({ changelog }: Props): JSX.Element {
|
||||
const formattedReleaseDate = dayjs(changelog.release_date).format(
|
||||
'MMMM D, YYYY',
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="changelog-renderer">
|
||||
<div className="changelog-renderer-line">
|
||||
<div className="inner-ball" />
|
||||
</div>
|
||||
<span className="changelog-release-date">{formattedReleaseDate}</span>
|
||||
{changelog.features && changelog.features.length > 0 && (
|
||||
<div className="changelog-renderer-list flex flex-col gap-7">
|
||||
{changelog.features.map((feature) => (
|
||||
<div key={feature.id}>
|
||||
<h2>{feature.title}</h2>
|
||||
{feature.media && renderMedia(feature.media)}
|
||||
<ReactMarkdown>{feature.description}</ReactMarkdown>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{changelog.bug_fixes && changelog.bug_fixes.length > 0 && (
|
||||
<div>
|
||||
<h2>Bug Fixes</h2>
|
||||
{changelog.bug_fixes && (
|
||||
<ReactMarkdown>{changelog.bug_fixes}</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{changelog.maintenance && changelog.maintenance.length > 0 && (
|
||||
<div>
|
||||
<h2>Maintenance</h2>
|
||||
{changelog.maintenance && (
|
||||
<ReactMarkdown>{changelog.maintenance}</ReactMarkdown>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChangelogRenderer;
|
||||
@@ -30,5 +30,5 @@ export enum LOCALSTORAGE {
|
||||
SHOW_EXCEPTIONS_QUICK_FILTERS = 'SHOW_EXCEPTIONS_QUICK_FILTERS',
|
||||
BANNER_DISMISSED = 'BANNER_DISMISSED',
|
||||
QUICK_FILTERS_SETTINGS_ANNOUNCEMENT = 'QUICK_FILTERS_SETTINGS_ANNOUNCEMENT',
|
||||
UNEXECUTED_FUNNELS = 'UNEXECUTED_FUNNELS',
|
||||
FUNNEL_STEPS = 'FUNNEL_STEPS',
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const ROUTES = {
|
||||
ALERT_OVERVIEW: '/alerts/overview',
|
||||
ALL_CHANNELS: '/settings/channels',
|
||||
CHANNELS_NEW: '/settings/channels/new',
|
||||
CHANNELS_EDIT: '/settings/channels/edit/:id',
|
||||
CHANNELS_EDIT: '/settings/channels/edit/:channelId',
|
||||
ALL_ERROR: '/exceptions',
|
||||
ERROR_DETAIL: '/error-detail',
|
||||
VERSION: '/status',
|
||||
@@ -62,8 +62,10 @@ const ROUTES = {
|
||||
WORKSPACE_SUSPENDED: '/workspace-suspended',
|
||||
SHORTCUTS: '/settings/shortcuts',
|
||||
INTEGRATIONS: '/integrations',
|
||||
MESSAGING_QUEUES_BASE: '/messaging-queues',
|
||||
MESSAGING_QUEUES_KAFKA: '/messaging-queues/kafka',
|
||||
MESSAGING_QUEUES_KAFKA_DETAIL: '/messaging-queues/kafka/detail',
|
||||
INFRASTRUCTURE_MONITORING_BASE: '/infrastructure-monitoring',
|
||||
INFRASTRUCTURE_MONITORING_HOSTS: '/infrastructure-monitoring/hosts',
|
||||
INFRASTRUCTURE_MONITORING_KUBERNETES: '/infrastructure-monitoring/kubernetes',
|
||||
MESSAGING_QUEUES_CELERY_TASK: '/messaging-queues/celery-task',
|
||||
@@ -71,6 +73,7 @@ const ROUTES = {
|
||||
METRICS_EXPLORER: '/metrics-explorer/summary',
|
||||
METRICS_EXPLORER_EXPLORER: '/metrics-explorer/explorer',
|
||||
METRICS_EXPLORER_VIEWS: '/metrics-explorer/views',
|
||||
API_MONITORING_BASE: '/api-monitoring',
|
||||
API_MONITORING: '/api-monitoring/explorer',
|
||||
METRICS_EXPLORER_BASE: '/metrics-explorer',
|
||||
WORKSPACE_ACCESS_RESTRICTED: '/workspace-access-restricted',
|
||||
|
||||
@@ -23,7 +23,7 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
||||
const onClickEditHandler = useCallback((id: string) => {
|
||||
history.push(
|
||||
generatePath(ROUTES.CHANNELS_EDIT, {
|
||||
id,
|
||||
channelId: id,
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
|
||||
@@ -1,8 +1,34 @@
|
||||
.app-banner-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
&.isWorkspaceRestricted {
|
||||
height: calc(100% - 32px);
|
||||
|
||||
// same styles as its either trial expired or payment failed
|
||||
&.isTrialExpired {
|
||||
height: calc(100% - 64px);
|
||||
}
|
||||
|
||||
&.isPaymentFailed {
|
||||
height: calc(100% - 64px);
|
||||
}
|
||||
}
|
||||
|
||||
&.isTrialExpired {
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
|
||||
&.isPaymentFailed {
|
||||
height: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.app-content {
|
||||
width: calc(100% - 64px); // width of the sidebar
|
||||
z-index: 0;
|
||||
@@ -163,3 +189,9 @@
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-restricted-banner,
|
||||
.trial-expiry-banner,
|
||||
.payment-failed-banner {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as Sentry from '@sentry/react';
|
||||
import { Flex } from 'antd';
|
||||
import getLocalStorageApi from 'api/browser/localstorage/get';
|
||||
import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import getChangelogByVersion from 'api/changelog/getChangelogByVersion';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import manageCreditCardApi from 'api/v1/portal/create';
|
||||
import getUserLatestVersion from 'api/v1/version/getLatestVersion';
|
||||
@@ -40,9 +41,10 @@ import {
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQueries } from 'react-query';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
UPDATE_CURRENT_ERROR,
|
||||
@@ -50,15 +52,18 @@ import {
|
||||
UPDATE_LATEST_VERSION,
|
||||
UPDATE_LATEST_VERSION_ERROR,
|
||||
} from 'types/actions/app';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { ErrorResponse, SuccessResponse, SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||
import APIError from 'types/api/error';
|
||||
import {
|
||||
LicenseEvent,
|
||||
LicensePlatform,
|
||||
LicenseState,
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { checkVersionState } from 'utils/app';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
import {
|
||||
getFormattedDate,
|
||||
@@ -81,6 +86,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
isFetchingFeatureFlags,
|
||||
featureFlagsFetchError,
|
||||
userPreferences,
|
||||
updateChangelog,
|
||||
} = useAppContext();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
@@ -92,6 +98,15 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
||||
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||
const [shouldFetchChangelog, setShouldFetchChangelog] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
|
||||
const { currentVersion, latestVersion } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const isLatestVersion = checkVersionState(currentVersion, latestVersion);
|
||||
|
||||
const handleBillingOnSuccess = (
|
||||
data: SuccessResponseV2<CheckoutSuccessPayloadProps>,
|
||||
@@ -129,7 +144,11 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const [getUserVersionResponse, getUserLatestVersionResponse] = useQueries([
|
||||
const [
|
||||
getUserVersionResponse,
|
||||
getUserLatestVersionResponse,
|
||||
getChangelogByVersionResponse,
|
||||
] = useQueries([
|
||||
{
|
||||
queryFn: getUserVersion,
|
||||
queryKey: ['getUserVersion', user?.accessJwt],
|
||||
@@ -140,6 +159,12 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
queryKey: ['getUserLatestVersion', user?.accessJwt],
|
||||
enabled: isLoggedIn,
|
||||
},
|
||||
{
|
||||
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
|
||||
getChangelogByVersion(latestVersion),
|
||||
queryKey: ['getChangelogByVersion', latestVersion],
|
||||
enabled: isLoggedIn && !isCloudUserVal && shouldFetchChangelog,
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -242,6 +267,30 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
notifications,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLatestVersion) {
|
||||
setShouldFetchChangelog(true);
|
||||
}
|
||||
}, [isLatestVersion]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
getChangelogByVersionResponse.isFetched &&
|
||||
getChangelogByVersionResponse.isSuccess &&
|
||||
getChangelogByVersionResponse.data &&
|
||||
getChangelogByVersionResponse.data.payload
|
||||
) {
|
||||
updateChangelog(getChangelogByVersionResponse.data.payload);
|
||||
}
|
||||
}, [
|
||||
updateChangelog,
|
||||
getChangelogByVersionResponse.isFetched,
|
||||
getChangelogByVersionResponse.isLoading,
|
||||
getChangelogByVersionResponse.isError,
|
||||
getChangelogByVersionResponse.data,
|
||||
getChangelogByVersionResponse.isSuccess,
|
||||
]);
|
||||
|
||||
const isToDisplayLayout = isLoggedIn;
|
||||
|
||||
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
|
||||
@@ -551,53 +600,63 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
(preference) => preference.name === USER_PREFERENCES.SIDENAV_PINNED,
|
||||
)?.value as boolean;
|
||||
|
||||
const SHOW_TRIAL_EXPIRY_BANNER =
|
||||
showTrialExpiryBanner && !showPaymentFailedWarning;
|
||||
const SHOW_WORKSPACE_RESTRICTED_BANNER = showWorkspaceRestricted;
|
||||
const SHOW_PAYMENT_FAILED_BANNER =
|
||||
!showTrialExpiryBanner && showPaymentFailedWarning;
|
||||
|
||||
return (
|
||||
<Layout className={cx(isDarkMode ? 'darkMode dark' : 'lightMode')}>
|
||||
<Helmet>
|
||||
<title>{pageTitle}</title>
|
||||
</Helmet>
|
||||
|
||||
{showTrialExpiryBanner && !showPaymentFailedWarning && (
|
||||
<div className="trial-expiry-banner">
|
||||
You are in free trial period. Your free trial will end on{' '}
|
||||
<span>{getFormattedDate(trialInfo?.trialEnd || Date.now())}.</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||
upgrade
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
'Please contact your administrator for upgrading to a paid plan.'
|
||||
{isLoggedIn && (
|
||||
<div className={cx('app-banner-container')}>
|
||||
{SHOW_TRIAL_EXPIRY_BANNER && (
|
||||
<div className="trial-expiry-banner">
|
||||
You are in free trial period. Your free trial will end on{' '}
|
||||
<span>{getFormattedDate(trialInfo?.trialEnd || Date.now())}.</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleUpgrade}>
|
||||
upgrade
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
'Please contact your administrator for upgrading to a paid plan.'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showWorkspaceRestricted && renderWorkspaceRestrictedBanner()}
|
||||
{SHOW_WORKSPACE_RESTRICTED_BANNER && renderWorkspaceRestrictedBanner()}
|
||||
|
||||
{!showTrialExpiryBanner && showPaymentFailedWarning && (
|
||||
<div className="payment-failed-banner">
|
||||
Your bill payment has failed. Your workspace will get suspended on{' '}
|
||||
<span>
|
||||
{getFormattedDateWithMinutes(
|
||||
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
|
||||
)}
|
||||
.
|
||||
</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleFailedPayment}>
|
||||
pay the bill
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
' Please contact your administrator to pay the bill.'
|
||||
{SHOW_PAYMENT_FAILED_BANNER && (
|
||||
<div className="payment-failed-banner">
|
||||
Your bill payment has failed. Your workspace will get suspended on{' '}
|
||||
<span>
|
||||
{getFormattedDateWithMinutes(
|
||||
dayjs(activeLicense?.event_queue?.scheduled_at).unix() || Date.now(),
|
||||
)}
|
||||
.
|
||||
</span>
|
||||
{user.role === USER_ROLES.ADMIN ? (
|
||||
<span>
|
||||
{' '}
|
||||
Please{' '}
|
||||
<a className="upgrade-link" onClick={handleFailedPayment}>
|
||||
pay the bill
|
||||
</a>
|
||||
to continue using SigNoz features.
|
||||
</span>
|
||||
) : (
|
||||
' Please contact your administrator to pay the bill.'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -607,6 +666,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
'app-layout',
|
||||
isDarkMode ? 'darkMode dark' : 'lightMode',
|
||||
sideNavPinned ? 'side-nav-pinned' : '',
|
||||
SHOW_WORKSPACE_RESTRICTED_BANNER ? 'isWorkspaceRestricted' : '',
|
||||
SHOW_TRIAL_EXPIRY_BANNER ? 'isTrialExpired' : '',
|
||||
SHOW_PAYMENT_FAILED_BANNER ? 'isPaymentFailed' : '',
|
||||
)}
|
||||
>
|
||||
{isToDisplayLayout && !renderFullScreen && (
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function EditAlertChannels({
|
||||
@@ -53,7 +52,11 @@ function EditAlertChannels({
|
||||
const [savingState, setSavingState] = useState<boolean>(false);
|
||||
const [testingState, setTestingState] = useState<boolean>(false);
|
||||
const { notifications } = useNotifications();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
// Extract channelId from URL pathname since useParams doesn't work in nested routing
|
||||
const { pathname } = window.location;
|
||||
const channelIdMatch = pathname.match(/\/settings\/channels\/edit\/([^/]+)/);
|
||||
const id = channelIdMatch ? channelIdMatch[1] : '';
|
||||
|
||||
const [type, setType] = useState<ChannelType>(
|
||||
initialValue?.type ? (initialValue.type as ChannelType) : ChannelType.Slack,
|
||||
|
||||
@@ -149,7 +149,7 @@ function FormAlertChannels({
|
||||
</Button>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
history.replace(ROUTES.SETTINGS);
|
||||
history.replace(ROUTES.ALL_CHANNELS);
|
||||
}}
|
||||
>
|
||||
{t('button_return')}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from '../HostsEmptyOrIncorrectMetrics';
|
||||
|
||||
describe('HostsEmptyOrIncorrectMetrics', () => {
|
||||
it('shows no data message when noData is true', () => {
|
||||
render(<HostsEmptyOrIncorrectMetrics noData incorrectData={false} />);
|
||||
expect(
|
||||
screen.getByText('No host metrics data received yet.'),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Infrastructure monitoring requires the/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows incorrect data message when incorrectData is true', () => {
|
||||
render(<HostsEmptyOrIncorrectMetrics noData={false} incorrectData />);
|
||||
expect(
|
||||
screen.getByText(
|
||||
'To see host metrics, upgrade to the latest version of SigNoz k8s-infra chart. Please contact support if you need help.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show no data message when noData is false', () => {
|
||||
render(<HostsEmptyOrIncorrectMetrics noData={false} incorrectData={false} />);
|
||||
expect(
|
||||
screen.queryByText('No host metrics data received yet.'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(/Infrastructure monitoring requires the/),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show incorrect data message when incorrectData is false', () => {
|
||||
render(<HostsEmptyOrIncorrectMetrics noData={false} incorrectData={false} />);
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'To see host metrics, upgrade to the latest version of SigNoz k8s-infra chart. Please contact support if you need help.',
|
||||
),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
/* eslint-disable react/button-has-type */
|
||||
import { render } from '@testing-library/react';
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as useGetHostListHooks from 'hooks/infraMonitoring/useGetHostList';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import store from 'store';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
|
||||
import HostsList from '../HostsList';
|
||||
|
||||
jest.mock('lib/getMinMax', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(() => ({
|
||||
minTime: 1713734400000,
|
||||
maxTime: 1713738000000,
|
||||
isValidTimeFormat: jest.fn().mockReturnValue(true),
|
||||
})),
|
||||
}));
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onSelect, selectedTime, selectedValue }: any): JSX.Element => (
|
||||
<div data-testid="custom-time-picker">
|
||||
<button onClick={(): void => onSelect('custom')}>
|
||||
{selectedTime} - {selectedValue}
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: (): any => ({
|
||||
globalTime: {
|
||||
selectedTime: {
|
||||
startTime: 1713734400000,
|
||||
endTime: 1713738000000,
|
||||
},
|
||||
maxTime: 1713738000000,
|
||||
minTime: 1713734400000,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn().mockReturnValue({
|
||||
pathname: ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||
}),
|
||||
}));
|
||||
jest.mock('react-router-dom-v5-compat', () => {
|
||||
const actual = jest.requireActual('react-router-dom-v5-compat');
|
||||
return {
|
||||
...actual,
|
||||
useSearchParams: jest
|
||||
.fn()
|
||||
.mockReturnValue([
|
||||
{ get: jest.fn(), entries: jest.fn().mockReturnValue([]) },
|
||||
jest.fn(),
|
||||
]),
|
||||
useNavigationType: (): any => 'PUSH',
|
||||
};
|
||||
});
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.spyOn(timezoneHooks, 'useTimezone').mockReturnValue({
|
||||
timezone: {
|
||||
offset: 0,
|
||||
},
|
||||
browserTimezone: {
|
||||
offset: 0,
|
||||
},
|
||||
} as any);
|
||||
jest.spyOn(useGetHostListHooks, 'useGetHostList').mockReturnValue({
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
records: [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
memory: 0.65,
|
||||
wait: 0.03,
|
||||
},
|
||||
],
|
||||
isSendingK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
} as any);
|
||||
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
user: {
|
||||
role: 'admin',
|
||||
},
|
||||
activeLicenseV3: {
|
||||
event_queue: {
|
||||
created_at: '0',
|
||||
event: LicenseEvent.NO_EVENT,
|
||||
scheduled_at: '0',
|
||||
status: '',
|
||||
updated_at: '0',
|
||||
},
|
||||
license: {
|
||||
license_key: 'test-license-key',
|
||||
license_type: 'trial',
|
||||
org_id: 'test-org-id',
|
||||
plan_id: 'test-plan-id',
|
||||
plan_name: 'test-plan-name',
|
||||
plan_type: 'trial',
|
||||
plan_version: 'test-plan-version',
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
|
||||
describe('HostsList', () => {
|
||||
it('renders hosts list table', () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders filters', () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import HostsListControls from '../HostsListControls';
|
||||
|
||||
jest.mock('container/QueryBuilder/filters/QueryBuilderSearch', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="query-builder-search">Search</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('HostsListControls', () => {
|
||||
const mockHandleFiltersChange = jest.fn();
|
||||
const mockFilters = {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
};
|
||||
|
||||
it('renders search and date time filters', () => {
|
||||
render(
|
||||
<HostsListControls
|
||||
handleFiltersChange={mockHandleFiltersChange}
|
||||
filters={mockFilters}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('query-builder-search')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('date-time-selection')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import HostsListTable from '../HostsListTable';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
|
||||
|
||||
describe('HostsListTable', () => {
|
||||
const mockHost = {
|
||||
hostName: 'test-host-1',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
memory: 0.65,
|
||||
wait: 0.03,
|
||||
load15: 1.5,
|
||||
os: 'linux',
|
||||
};
|
||||
|
||||
const mockTableData = {
|
||||
payload: {
|
||||
data: {
|
||||
hosts: [mockHost],
|
||||
},
|
||||
},
|
||||
};
|
||||
const mockOnHostClick = jest.fn();
|
||||
const mockSetCurrentPage = jest.fn();
|
||||
const mockSetOrderBy = jest.fn();
|
||||
const mockSetPageSize = jest.fn();
|
||||
const mockProps = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
tableData: mockTableData,
|
||||
hostMetricsData: [mockHost],
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
onHostClick: mockOnHostClick,
|
||||
currentPage: 1,
|
||||
setCurrentPage: mockSetCurrentPage,
|
||||
pageSize: 10,
|
||||
setOrderBy: mockSetOrderBy,
|
||||
setPageSize: mockSetPageSize,
|
||||
} as any;
|
||||
|
||||
it('renders loading state if isLoading is true', () => {
|
||||
const { container } = render(<HostsListTable {...mockProps} isLoading />);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders loading state if isFetching is true', () => {
|
||||
const { container } = render(<HostsListTable {...mockProps} isFetching />);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders error state if isError is true', () => {
|
||||
render(<HostsListTable {...mockProps} isError />);
|
||||
expect(screen.getByText('Something went wrong')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if no hosts are found', () => {
|
||||
const { container } = render(<HostsListTable {...mockProps} />);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if sentAnyHostMetricsData is false', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
sentAnyHostMetricsData: false,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if isSendingIncorrectK8SAgentMetrics is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
isSendingIncorrectK8SAgentMetrics: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders table data', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
isSendingIncorrectK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { formatDataForTable, GetHostsQuickFiltersConfig } from '../utils';
|
||||
|
||||
const PROGRESS_BAR_CLASS = '.progress-bar';
|
||||
|
||||
jest.mock('uplot', () => {
|
||||
const paths = {
|
||||
spline: jest.fn(),
|
||||
bars: jest.fn(),
|
||||
};
|
||||
const uplotMock = jest.fn(() => ({
|
||||
paths,
|
||||
}));
|
||||
return {
|
||||
paths,
|
||||
default: uplotMock,
|
||||
};
|
||||
});
|
||||
|
||||
describe('InfraMonitoringHosts utils', () => {
|
||||
describe('formatDataForTable', () => {
|
||||
it('should format host data correctly', () => {
|
||||
const mockData = [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: true,
|
||||
cpu: 0.95,
|
||||
memory: 0.85,
|
||||
wait: 0.05,
|
||||
load15: 2.5,
|
||||
os: 'linux',
|
||||
},
|
||||
] as any;
|
||||
|
||||
const result = formatDataForTable(mockData);
|
||||
|
||||
expect(result[0].hostName).toBe('test-host');
|
||||
expect(result[0].wait).toBe('5%');
|
||||
expect(result[0].load15).toBe(2.5);
|
||||
|
||||
// Test active tag rendering
|
||||
const activeTag = render(result[0].active as JSX.Element);
|
||||
expect(activeTag.container.textContent).toBe('ACTIVE');
|
||||
expect(activeTag.container.querySelector('.active')).toBeTruthy();
|
||||
|
||||
// Test CPU progress bar
|
||||
const cpuProgress = render(result[0].cpu as JSX.Element);
|
||||
const cpuProgressBar = cpuProgress.container.querySelector(
|
||||
PROGRESS_BAR_CLASS,
|
||||
);
|
||||
expect(cpuProgressBar).toBeTruthy();
|
||||
|
||||
// Test memory progress bar
|
||||
const memoryProgress = render(result[0].memory as JSX.Element);
|
||||
const memoryProgressBar = memoryProgress.container.querySelector(
|
||||
PROGRESS_BAR_CLASS,
|
||||
);
|
||||
expect(memoryProgressBar).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle inactive hosts', () => {
|
||||
const mockData = [
|
||||
{
|
||||
hostName: 'test-host',
|
||||
active: false,
|
||||
cpu: 0.3,
|
||||
memory: 0.4,
|
||||
wait: 0.02,
|
||||
load15: 1.2,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: [],
|
||||
memoryTimeSeries: [],
|
||||
waitTimeSeries: [],
|
||||
load15TimeSeries: [],
|
||||
},
|
||||
] as any;
|
||||
|
||||
const result = formatDataForTable(mockData);
|
||||
|
||||
const inactiveTag = render(result[0].active as JSX.Element);
|
||||
expect(inactiveTag.container.textContent).toBe('INACTIVE');
|
||||
expect(inactiveTag.container.querySelector('.inactive')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GetHostsQuickFiltersConfig', () => {
|
||||
it('should return correct config when dotMetricsEnabled is true', () => {
|
||||
const result = GetHostsQuickFiltersConfig(true);
|
||||
|
||||
expect(result[0].attributeKey.key).toBe('host.name');
|
||||
expect(result[1].attributeKey.key).toBe('os.type');
|
||||
expect(result[0].aggregateAttribute).toBe('system.cpu.load_average.15m');
|
||||
});
|
||||
|
||||
it('should return correct config when dotMetricsEnabled is false', () => {
|
||||
const result = GetHostsQuickFiltersConfig(false);
|
||||
|
||||
expect(result[0].attributeKey.key).toBe('host_name');
|
||||
expect(result[1].attributeKey.key).toBe('os_type');
|
||||
expect(result[0].aggregateAttribute).toBe('system_cpu_load_average_15m');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -611,9 +611,7 @@ export const errorPercentage = ({
|
||||
{
|
||||
id: '',
|
||||
key: {
|
||||
key: dotMetricsEnabled
|
||||
? WidgetKeys.Service_name
|
||||
: WidgetKeys.StatusCodeNorm,
|
||||
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
|
||||
dataType: DataTypes.Int64,
|
||||
isColumn: false,
|
||||
type: MetricsType.Tag,
|
||||
|
||||
@@ -240,6 +240,7 @@
|
||||
line-height: 18px; /* 163.636% */
|
||||
letter-spacing: 0.88px;
|
||||
text-transform: uppercase;
|
||||
min-height: 18px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -889,6 +890,10 @@
|
||||
.ant-dropdown-menu-item-divider {
|
||||
background-color: var(--Slate-500, #161922) !important;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-dropdown,
|
||||
|
||||
@@ -23,6 +23,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import { Logout } from 'api/utils';
|
||||
import updateUserPreference from 'api/v1/user/preferences/name/update';
|
||||
import cx from 'classnames';
|
||||
import ChangelogModal from 'components/ChangelogModal/ChangelogModal';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { GlobalShortcuts } from 'constants/shortcuts/globalShortcuts';
|
||||
@@ -124,6 +125,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
trialInfo,
|
||||
isLoggedIn,
|
||||
userPreferences,
|
||||
changelog,
|
||||
updateUserPreferenceInContext,
|
||||
} = useAppContext();
|
||||
|
||||
@@ -155,6 +157,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
|
||||
const [hasScroll, setHasScroll] = useState(false);
|
||||
const navTopSectionRef = useRef<HTMLDivElement>(null);
|
||||
const [showChangelogModal, setShowChangelogModal] = useState<boolean>(false);
|
||||
|
||||
const checkScroll = useCallback((): void => {
|
||||
if (navTopSectionRef.current) {
|
||||
@@ -447,6 +450,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
{
|
||||
key: 'workspace',
|
||||
label: 'Workspace Settings',
|
||||
disabled: isWorkspaceBlocked,
|
||||
},
|
||||
...(isEnterpriseSelfHostedUser || isCommunityEnterpriseUser
|
||||
? [
|
||||
@@ -464,7 +468,12 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
),
|
||||
},
|
||||
].filter(Boolean),
|
||||
[isEnterpriseSelfHostedUser, isCommunityEnterpriseUser, user.email],
|
||||
[
|
||||
isEnterpriseSelfHostedUser,
|
||||
isCommunityEnterpriseUser,
|
||||
user.email,
|
||||
isWorkspaceBlocked,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -731,20 +740,13 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isCloudUser, isEnterpriseSelfHostedUser]);
|
||||
|
||||
const onClickVersionHandler = useCallback(
|
||||
(event: MouseEvent): void => {
|
||||
if (isCloudUser) {
|
||||
return;
|
||||
}
|
||||
const onClickVersionHandler = useCallback((): void => {
|
||||
if (isCloudUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCtrlMetaKey(event)) {
|
||||
openInNewTab(ROUTES.VERSION);
|
||||
} else {
|
||||
history.push(ROUTES.VERSION);
|
||||
}
|
||||
},
|
||||
[isCloudUser],
|
||||
);
|
||||
setShowChangelogModal(true);
|
||||
}, [isCloudUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLatestVersion && !isCloudUser) {
|
||||
@@ -784,7 +786,9 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
'brand-title-section',
|
||||
isCommunityEnterpriseUser && 'community-enterprise-user',
|
||||
isCloudUser && 'cloud-user',
|
||||
showVersionUpdateNotification && 'version-update-notification',
|
||||
showVersionUpdateNotification &&
|
||||
changelog &&
|
||||
'version-update-notification',
|
||||
)}
|
||||
>
|
||||
<span className="license-type"> {licenseTag} </span>
|
||||
@@ -795,7 +799,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
overlayClassName="version-tooltip-overlay"
|
||||
arrow={false}
|
||||
overlay={
|
||||
showVersionUpdateNotification && (
|
||||
showVersionUpdateNotification &&
|
||||
changelog && (
|
||||
<div className="version-update-notification-tooltip">
|
||||
<div className="version-update-notification-tooltip-title">
|
||||
There's a new version available.
|
||||
@@ -813,7 +818,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
{currentVersion}
|
||||
</span>
|
||||
|
||||
{showVersionUpdateNotification && (
|
||||
{showVersionUpdateNotification && changelog && (
|
||||
<span className="version-update-notification-dot-icon" />
|
||||
)}
|
||||
</div>
|
||||
@@ -1044,6 +1049,9 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{showChangelogModal && (
|
||||
<ChangelogModal onClose={(): void => setShowChangelogModal(false)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ export const helpSupportDropdownMenuItems: SidebarItem[] = [
|
||||
},
|
||||
{
|
||||
key: 'invite-collaborators',
|
||||
label: 'Invite a Collaborator',
|
||||
label: 'Invite a Team Member',
|
||||
icon: <Plus size={14} />,
|
||||
itemKey: 'invite-collaborators',
|
||||
},
|
||||
@@ -403,6 +403,10 @@ export const NEW_ROUTES_MENU_ITEM_KEY_MAP: Record<string, string> = {
|
||||
[ROUTES.TRACE_EXPLORER]: ROUTES.TRACES_EXPLORER,
|
||||
[ROUTES.LOGS_BASE]: ROUTES.LOGS_EXPLORER,
|
||||
[ROUTES.METRICS_EXPLORER_BASE]: ROUTES.METRICS_EXPLORER,
|
||||
[ROUTES.INFRASTRUCTURE_MONITORING_BASE]:
|
||||
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||
[ROUTES.API_MONITORING_BASE]: ROUTES.API_MONITORING,
|
||||
[ROUTES.MESSAGING_QUEUES_BASE]: ROUTES.MESSAGING_QUEUES_OVERVIEW,
|
||||
};
|
||||
|
||||
export default menuItems;
|
||||
|
||||
@@ -241,6 +241,15 @@
|
||||
&-title {
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
|
||||
&-footer {
|
||||
border-top-color: var(--bg-vanilla-300);
|
||||
background: var(--bg-vanilla-100);
|
||||
.add-span-to-funnel-modal__discard-button {
|
||||
background: var(--bg-vanilla-200);
|
||||
color: var(--bg-ink-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ function FunnelDetailsView({
|
||||
funnel={funnel}
|
||||
isTraceDetailsPage
|
||||
span={span}
|
||||
disableAutoSave
|
||||
triggerAutoSave={triggerAutoSave}
|
||||
showNotifications={showNotifications}
|
||||
/>
|
||||
@@ -143,13 +142,19 @@ function AddSpanToFunnelModal({
|
||||
const handleSaveFunnel = (): void => {
|
||||
setTriggerSave(true);
|
||||
// Reset trigger after a brief moment to allow the save to be processed
|
||||
setTimeout(() => setTriggerSave(false), 100);
|
||||
setTimeout(() => {
|
||||
setTriggerSave(false);
|
||||
onClose();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const handleDiscard = (): void => {
|
||||
setTriggerDiscard(true);
|
||||
// Reset trigger after a brief moment
|
||||
setTimeout(() => setTriggerDiscard(false), 100);
|
||||
setTimeout(() => {
|
||||
setTriggerDiscard(false);
|
||||
onClose();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const renderListView = (): JSX.Element => (
|
||||
@@ -239,9 +244,6 @@ function AddSpanToFunnelModal({
|
||||
footer={
|
||||
activeView === ModalView.DETAILS
|
||||
? [
|
||||
<Button key="close" onClick={onClose}>
|
||||
Close
|
||||
</Button>,
|
||||
<Button
|
||||
type="default"
|
||||
key="discard"
|
||||
|
||||
@@ -149,30 +149,28 @@ function SpanOverview({
|
||||
<Typography.Text className="service-name">
|
||||
{span.serviceName}
|
||||
</Typography.Text>
|
||||
{!!span.serviceName &&
|
||||
!!span.name &&
|
||||
process.env.NODE_ENV === 'development' && (
|
||||
<div className="add-funnel-button">
|
||||
<span className="add-funnel-button__separator">·</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="add-funnel-button__button"
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleAddSpanToFunnel(span);
|
||||
}}
|
||||
icon={
|
||||
<img
|
||||
className="add-funnel-button__icon"
|
||||
src="/Icons/funnel-add.svg"
|
||||
alt="funnel-icon"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!!span.serviceName && !!span.name && (
|
||||
<div className="add-funnel-button">
|
||||
<span className="add-funnel-button__separator">·</span>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
className="add-funnel-button__button"
|
||||
onClick={(e): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleAddSpanToFunnel(span);
|
||||
}}
|
||||
icon={
|
||||
<img
|
||||
className="add-funnel-button__icon"
|
||||
src="/Icons/funnel-add.svg"
|
||||
alt="funnel-icon"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -475,7 +473,7 @@ function Success(props: ISuccessProps): JSX.Element {
|
||||
virtualiserRef={virtualizerRef}
|
||||
setColumnWidths={setTraceFlamegraphStatsWidth}
|
||||
/>
|
||||
{selectedSpanToAddToFunnel && process.env.NODE_ENV === 'development' && (
|
||||
{selectedSpanToAddToFunnel && (
|
||||
<AddSpanToFunnelModal
|
||||
span={selectedSpanToAddToFunnel}
|
||||
isOpen={isAddSpanToFunnelModalOpen}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { FunnelData, FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
import { useUpdateFunnelSteps } from './useFunnels';
|
||||
@@ -13,22 +16,30 @@ interface UseFunnelConfiguration {
|
||||
isPopoverOpen: boolean;
|
||||
setIsPopoverOpen: (isPopoverOpen: boolean) => void;
|
||||
steps: FunnelStepData[];
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
// Add this helper function
|
||||
const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
|
||||
export const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
|
||||
if (steps.some((step) => !step.filters)) return steps;
|
||||
|
||||
return steps.map((step) => ({
|
||||
...step,
|
||||
filters: {
|
||||
...step.filters,
|
||||
items: step.filters.items.map((item) => ({
|
||||
id: '',
|
||||
key: item.key,
|
||||
value: item.value,
|
||||
op: item.op,
|
||||
})),
|
||||
items: step.filters.items.map((item) => {
|
||||
const {
|
||||
id: unusedId,
|
||||
isIndexed,
|
||||
...keyObj
|
||||
} = item.key as BaseAutocompleteData;
|
||||
return {
|
||||
id: '',
|
||||
key: keyObj,
|
||||
value: item.value,
|
||||
op: item.op,
|
||||
};
|
||||
}),
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -36,22 +47,22 @@ const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function useFunnelConfiguration({
|
||||
funnel,
|
||||
disableAutoSave = false,
|
||||
triggerAutoSave = false,
|
||||
showNotifications = false,
|
||||
}: {
|
||||
funnel: FunnelData;
|
||||
disableAutoSave?: boolean;
|
||||
triggerAutoSave?: boolean;
|
||||
showNotifications?: boolean;
|
||||
}): UseFunnelConfiguration {
|
||||
const { notifications } = useNotifications();
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
steps,
|
||||
initialSteps,
|
||||
hasIncompleteStepFields,
|
||||
lastUpdatedSteps,
|
||||
setLastUpdatedSteps,
|
||||
handleRestoreSteps,
|
||||
handleRunFunnel,
|
||||
selectedTime,
|
||||
setIsUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
|
||||
// State management
|
||||
@@ -59,10 +70,6 @@ export default function useFunnelConfiguration({
|
||||
|
||||
const debouncedSteps = useDebounce(steps, 200);
|
||||
|
||||
const [lastValidatedSteps, setLastValidatedSteps] = useState<FunnelStepData[]>(
|
||||
initialSteps,
|
||||
);
|
||||
|
||||
// Mutation hooks
|
||||
const updateStepsMutation = useUpdateFunnelSteps(
|
||||
funnel.funnel_id,
|
||||
@@ -71,6 +78,15 @@ export default function useFunnelConfiguration({
|
||||
|
||||
// Derived state
|
||||
const lastSavedStepsStateRef = useRef<FunnelStepData[]>(steps);
|
||||
const hasRestoredFromLocalStorage = useRef(false);
|
||||
|
||||
// localStorage hook for funnel steps
|
||||
const localStorageKey = `${LOCALSTORAGE.FUNNEL_STEPS}_${funnel.funnel_id}`;
|
||||
const [
|
||||
localStorageSavedSteps,
|
||||
setLocalStorageSavedSteps,
|
||||
clearLocalStorageSavedSteps,
|
||||
] = useLocalStorage<FunnelStepData[] | null>(localStorageKey, null);
|
||||
|
||||
const hasStepsChanged = useCallback(() => {
|
||||
const normalizedLastSavedSteps = normalizeSteps(
|
||||
@@ -80,6 +96,34 @@ export default function useFunnelConfiguration({
|
||||
return !isEqual(normalizedDebouncedSteps, normalizedLastSavedSteps);
|
||||
}, [debouncedSteps]);
|
||||
|
||||
// Handle localStorage for funnel steps
|
||||
useEffect(() => {
|
||||
// Restore from localStorage on first run if
|
||||
if (!hasRestoredFromLocalStorage.current) {
|
||||
const savedSteps = localStorageSavedSteps;
|
||||
if (savedSteps) {
|
||||
handleRestoreSteps(savedSteps);
|
||||
hasRestoredFromLocalStorage.current = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Save steps to localStorage
|
||||
if (hasStepsChanged()) {
|
||||
setLocalStorageSavedSteps(debouncedSteps);
|
||||
}
|
||||
}, [
|
||||
debouncedSteps,
|
||||
funnel.funnel_id,
|
||||
hasStepsChanged,
|
||||
handleRestoreSteps,
|
||||
localStorageSavedSteps,
|
||||
setLocalStorageSavedSteps,
|
||||
queryClient,
|
||||
selectedTime,
|
||||
lastUpdatedSteps,
|
||||
]);
|
||||
|
||||
const hasFunnelStepDefinitionsChanged = useCallback(
|
||||
(prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean => {
|
||||
if (prevSteps.length !== nextSteps.length) return true;
|
||||
@@ -97,15 +141,6 @@ export default function useFunnelConfiguration({
|
||||
[],
|
||||
);
|
||||
|
||||
const hasFunnelLatencyTypeChanged = useCallback(
|
||||
(prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean =>
|
||||
prevSteps.some((step, index) => {
|
||||
const nextStep = nextSteps[index];
|
||||
return step.latency_type !== nextStep.latency_type;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// Mutation payload preparation
|
||||
const getUpdatePayload = useCallback(
|
||||
() => ({
|
||||
@@ -116,33 +151,19 @@ export default function useFunnelConfiguration({
|
||||
[funnel.funnel_id, debouncedSteps],
|
||||
);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedTime } = useFunnelContext();
|
||||
|
||||
const validateStepsQueryKey = useMemo(
|
||||
() => [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnel.funnel_id, selectedTime],
|
||||
[funnel.funnel_id, selectedTime],
|
||||
);
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect(() => {
|
||||
// Determine if we should save based on the mode
|
||||
let shouldSave = false;
|
||||
|
||||
if (disableAutoSave) {
|
||||
// Manual save mode: only save when explicitly triggered
|
||||
shouldSave = triggerAutoSave;
|
||||
} else {
|
||||
// Auto-save mode: save when steps have changed and no incomplete fields
|
||||
shouldSave = hasStepsChanged() && !hasIncompleteStepFields;
|
||||
}
|
||||
|
||||
if (shouldSave && !isEqual(debouncedSteps, lastValidatedSteps)) {
|
||||
if (triggerAutoSave && !isEqual(debouncedSteps, lastUpdatedSteps)) {
|
||||
setIsUpdatingFunnel(true);
|
||||
updateStepsMutation.mutate(getUpdatePayload(), {
|
||||
onSuccess: (data) => {
|
||||
const updatedFunnelSteps = data?.payload?.steps;
|
||||
|
||||
if (!updatedFunnelSteps) return;
|
||||
|
||||
// Clear localStorage since steps are saved successfully
|
||||
clearLocalStorageSavedSteps();
|
||||
|
||||
queryClient.setQueryData(
|
||||
[REACT_QUERY_KEY.GET_FUNNEL_DETAILS, funnel.funnel_id],
|
||||
(oldData: any) => {
|
||||
@@ -163,17 +184,9 @@ export default function useFunnelConfiguration({
|
||||
(step) => step.service_name === '' || step.span_name === '',
|
||||
);
|
||||
|
||||
if (hasFunnelLatencyTypeChanged(lastValidatedSteps, debouncedSteps)) {
|
||||
handleRunFunnel();
|
||||
setLastValidatedSteps(debouncedSteps);
|
||||
}
|
||||
// Only validate if funnel steps definitions
|
||||
else if (
|
||||
!hasIncompleteStepFields &&
|
||||
hasFunnelStepDefinitionsChanged(lastValidatedSteps, debouncedSteps)
|
||||
) {
|
||||
queryClient.refetchQueries(validateStepsQueryKey);
|
||||
setLastValidatedSteps(debouncedSteps);
|
||||
if (!hasIncompleteStepFields) {
|
||||
setLastUpdatedSteps(debouncedSteps);
|
||||
}
|
||||
|
||||
// Show success notification only when requested
|
||||
@@ -216,17 +229,18 @@ export default function useFunnelConfiguration({
|
||||
getUpdatePayload,
|
||||
hasFunnelStepDefinitionsChanged,
|
||||
hasStepsChanged,
|
||||
lastValidatedSteps,
|
||||
lastUpdatedSteps,
|
||||
queryClient,
|
||||
validateStepsQueryKey,
|
||||
triggerAutoSave,
|
||||
showNotifications,
|
||||
disableAutoSave,
|
||||
localStorageSavedSteps,
|
||||
clearLocalStorageSavedSteps,
|
||||
]);
|
||||
|
||||
return {
|
||||
isPopoverOpen,
|
||||
setIsPopoverOpen,
|
||||
steps,
|
||||
isSaving: updateStepsMutation.isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,10 +20,11 @@ export function useFunnelMetrics({
|
||||
metricsData: MetricItem[];
|
||||
conversionRate: number;
|
||||
} {
|
||||
const { startTime, endTime } = useFunnelContext();
|
||||
const { startTime, endTime, steps } = useFunnelContext();
|
||||
const payload = {
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
steps,
|
||||
};
|
||||
|
||||
const {
|
||||
@@ -81,6 +82,7 @@ export function useFunnelStepsMetrics({
|
||||
end_time: endTime,
|
||||
step_start: stepStart,
|
||||
step_end: stepEnd,
|
||||
steps,
|
||||
};
|
||||
|
||||
const {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
FunnelOverviewResponse,
|
||||
FunnelStepsOverviewPayload,
|
||||
FunnelStepsOverviewResponse,
|
||||
FunnelStepsPayload,
|
||||
FunnelStepsResponse,
|
||||
getFunnelById,
|
||||
getFunnelErrorTraces,
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
CreateFunnelPayload,
|
||||
CreateFunnelResponse,
|
||||
FunnelData,
|
||||
FunnelStepData,
|
||||
} from 'types/api/traceFunnels';
|
||||
|
||||
export const useFunnelsList = (): UseQueryResult<
|
||||
@@ -117,12 +119,14 @@ export const useValidateFunnelSteps = ({
|
||||
startTime,
|
||||
endTime,
|
||||
enabled,
|
||||
steps,
|
||||
}: {
|
||||
funnelId: string;
|
||||
selectedTime: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
enabled: boolean;
|
||||
steps: FunnelStepData[];
|
||||
}): UseQueryResult<
|
||||
SuccessResponse<ValidateFunnelResponse> | ErrorResponse,
|
||||
Error
|
||||
@@ -130,11 +134,19 @@ export const useValidateFunnelSteps = ({
|
||||
useQuery({
|
||||
queryFn: ({ signal }) =>
|
||||
validateFunnelSteps(
|
||||
funnelId,
|
||||
{ start_time: startTime, end_time: endTime },
|
||||
{ start_time: startTime, end_time: endTime, steps },
|
||||
signal,
|
||||
),
|
||||
queryKey: [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnelId, selectedTime],
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
steps.map((step) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { latency_type, ...rest } = step;
|
||||
return rest;
|
||||
}),
|
||||
],
|
||||
enabled,
|
||||
staleTime: 0,
|
||||
});
|
||||
@@ -168,18 +180,17 @@ export const useFunnelOverview = (
|
||||
const {
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
return useQuery({
|
||||
queryFn: ({ signal }) => getFunnelOverview(funnelId, payload, signal),
|
||||
queryFn: ({ signal }) => getFunnelOverview(payload, signal),
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
payload.step_start ?? '',
|
||||
payload.step_end ?? '',
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -190,18 +201,19 @@ export const useFunnelSlowTraces = (
|
||||
const {
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
return useQuery<SuccessResponse<SlowTraceData> | ErrorResponse, Error>({
|
||||
queryFn: ({ signal }) => getFunnelSlowTraces(funnelId, payload, signal),
|
||||
queryFn: ({ signal }) => getFunnelSlowTraces(payload, signal),
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
payload.step_start ?? '',
|
||||
payload.step_end ?? '',
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -212,7 +224,7 @@ export const useFunnelErrorTraces = (
|
||||
const {
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
return useQuery({
|
||||
queryFn: ({ signal }) => getFunnelErrorTraces(funnelId, payload, signal),
|
||||
@@ -222,35 +234,31 @@ export const useFunnelErrorTraces = (
|
||||
selectedTime,
|
||||
payload.step_start ?? '',
|
||||
payload.step_end ?? '',
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
};
|
||||
|
||||
export function useFunnelStepsGraphData(
|
||||
funnelId: string,
|
||||
payload: FunnelStepsPayload,
|
||||
): UseQueryResult<SuccessResponse<FunnelStepsResponse> | ErrorResponse, Error> {
|
||||
const {
|
||||
startTime,
|
||||
endTime,
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
|
||||
return useQuery({
|
||||
queryFn: ({ signal }) =>
|
||||
getFunnelSteps(
|
||||
funnelId,
|
||||
{ start_time: startTime, end_time: endTime },
|
||||
signal,
|
||||
),
|
||||
queryFn: ({ signal }) => getFunnelSteps(payload, signal),
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -264,17 +272,18 @@ export const useFunnelStepsOverview = (
|
||||
const {
|
||||
selectedTime,
|
||||
validTracesCount,
|
||||
hasFunnelBeenExecuted,
|
||||
isUpdatingFunnel,
|
||||
} = useFunnelContext();
|
||||
return useQuery({
|
||||
queryFn: ({ signal }) => getFunnelStepsOverview(funnelId, payload, signal),
|
||||
queryFn: ({ signal }) => getFunnelStepsOverview(payload, signal),
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_FUNNEL_STEPS_OVERVIEW,
|
||||
funnelId,
|
||||
selectedTime,
|
||||
payload.step_start ?? '',
|
||||
payload.step_end ?? '',
|
||||
payload.steps,
|
||||
],
|
||||
enabled: !!funnelId && validTracesCount > 0 && hasFunnelBeenExecuted,
|
||||
enabled: !!funnelId && validTracesCount > 0 && !isUpdatingFunnel,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -38,6 +38,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.alert-empty-card {
|
||||
margin-top: 50px;
|
||||
.ant-empty-description {
|
||||
color: var(--text-vanilla-400);
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.alert-empty-card {
|
||||
.ant-empty-description {
|
||||
color: var(--text-ink-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-details {
|
||||
margin-top: 10px;
|
||||
.divider {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import './AlertDetails.styles.scss';
|
||||
|
||||
import { Breadcrumb, Button, Divider } from 'antd';
|
||||
import { Breadcrumb, Button, Divider, Empty } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { Filters } from 'components/AlertDetailsFilters/Filters';
|
||||
import NotFound from 'components/NotFound';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import Spinner from 'components/Spinner';
|
||||
import ROUTES from 'constants/routes';
|
||||
@@ -70,6 +69,7 @@ BreadCrumbItem.defaultProps = {
|
||||
function AlertDetails(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
const { routes } = useRouteTabUtils();
|
||||
const { t } = useTranslation(['alerts']);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
@@ -90,7 +90,11 @@ function AlertDetails(): JSX.Element {
|
||||
!isValidRuleId ||
|
||||
(alertDetailsResponse && alertDetailsResponse.statusCode !== 200)
|
||||
) {
|
||||
return <NotFound />;
|
||||
return (
|
||||
<div className="alert-empty-card">
|
||||
<Empty description={t('alert_rule_not_found')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleTabChange = (route: string): void => {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
}
|
||||
|
||||
.ant-tabs-content-holder {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.edit-alert-channels-container {
|
||||
width: 90%;
|
||||
margin: 12px auto;
|
||||
margin: 12px;
|
||||
|
||||
border: 1px solid var(--Slate-500, #161922);
|
||||
background: var(--Ink-400, #121317);
|
||||
|
||||
@@ -15,23 +15,27 @@ import {
|
||||
import EditAlertChannels from 'container/EditAlertChannels';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Channels } from 'types/api/channels/getAll';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
function ChannelsEdit(): JSX.Element {
|
||||
const { id } = useParams<Params>();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Extract channelId from URL pathname since useParams doesn't work in nested routing
|
||||
const { pathname } = window.location;
|
||||
const channelIdMatch = pathname.match(/\/settings\/channels\/edit\/([^/]+)/);
|
||||
const channelId = channelIdMatch ? channelIdMatch[1] : undefined;
|
||||
|
||||
const { isFetching, isError, data, error } = useQuery<
|
||||
SuccessResponseV2<Channels>,
|
||||
APIError
|
||||
>(['getChannel', id], {
|
||||
>(['getChannel', channelId], {
|
||||
queryFn: () =>
|
||||
get({
|
||||
id,
|
||||
id: channelId || '',
|
||||
}),
|
||||
enabled: !!channelId,
|
||||
});
|
||||
|
||||
if (isError) {
|
||||
@@ -144,8 +148,5 @@ function ChannelsEdit(): JSX.Element {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
interface Params {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export default ChannelsEdit;
|
||||
|
||||
@@ -7,6 +7,7 @@ import NewWidget from 'container/NewWidget';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { generatePath, useLocation, useParams } from 'react-router-dom';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
@@ -52,11 +53,13 @@ function DashboardWidget(): JSX.Element | null {
|
||||
}
|
||||
|
||||
return (
|
||||
<NewWidget
|
||||
yAxisUnit={selectedWidget?.yAxisUnit}
|
||||
selectedGraph={selectedGraph}
|
||||
fillSpans={selectedWidget?.fillSpans}
|
||||
/>
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
yAxisUnit={selectedWidget?.yAxisUnit}
|
||||
selectedGraph={selectedGraph}
|
||||
fillSpans={selectedWidget?.fillSpans}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,14 @@ import ROUTES from 'constants/routes';
|
||||
import InfraMonitoringHosts from 'container/InfraMonitoringHosts';
|
||||
import InfraMonitoringK8s from 'container/InfraMonitoringK8s';
|
||||
import { Inbox } from 'lucide-react';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
|
||||
export const Hosts: TabRoutes = {
|
||||
Component: InfraMonitoringHosts,
|
||||
Component: (): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<InfraMonitoringHosts />
|
||||
</PreferenceContextProvider>
|
||||
),
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Inbox size={16} /> Hosts
|
||||
@@ -16,7 +21,11 @@ export const Hosts: TabRoutes = {
|
||||
};
|
||||
|
||||
export const Kubernetes: TabRoutes = {
|
||||
Component: InfraMonitoringK8s,
|
||||
Component: (): JSX.Element => (
|
||||
<PreferenceContextProvider>
|
||||
<InfraMonitoringK8s />
|
||||
</PreferenceContextProvider>
|
||||
),
|
||||
name: (
|
||||
<div className="tab-item">
|
||||
<Inbox size={16} /> Kubernetes
|
||||
|
||||
@@ -10,6 +10,7 @@ import LogsFilters from 'container/LogsFilters';
|
||||
import LogsSearchFilter from 'container/LogsSearchFilter';
|
||||
import LogsTable from 'container/LogsTable';
|
||||
import history from 'lib/history';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
@@ -82,69 +83,71 @@ function OldLogsExplorer(): JSX.Element {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="old-logs-explorer">
|
||||
<SpaceContainer
|
||||
split={<Divider type="vertical" />}
|
||||
align="center"
|
||||
direction="horizontal"
|
||||
>
|
||||
<LogsSearchFilter />
|
||||
<LogLiveTail />
|
||||
</SpaceContainer>
|
||||
<PreferenceContextProvider>
|
||||
<div className="old-logs-explorer">
|
||||
<SpaceContainer
|
||||
split={<Divider type="vertical" />}
|
||||
align="center"
|
||||
direction="horizontal"
|
||||
>
|
||||
<LogsSearchFilter />
|
||||
<LogLiveTail />
|
||||
</SpaceContainer>
|
||||
|
||||
<LogsAggregate />
|
||||
<LogsAggregate />
|
||||
|
||||
<Row gutter={20} wrap={false}>
|
||||
<LogsFilters />
|
||||
<Col flex={1} className="logs-col-container">
|
||||
<Row>
|
||||
<Col flex={1}>
|
||||
<Space align="baseline" direction="horizontal">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
value={selectedViewModeOption}
|
||||
onChange={onChangeVeiwMode}
|
||||
>
|
||||
{viewModeOptionList.map((option) => (
|
||||
<Select.Option key={option.value}>{option.label}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
{isFormatButtonVisible && (
|
||||
<Popover
|
||||
<Row gutter={20} wrap={false}>
|
||||
<LogsFilters />
|
||||
<Col flex={1} className="logs-col-container">
|
||||
<Row>
|
||||
<Col flex={1}>
|
||||
<Space align="baseline" direction="horizontal">
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
placement="right"
|
||||
content={renderPopoverContent}
|
||||
style={defaultSelectStyle}
|
||||
value={selectedViewModeOption}
|
||||
onChange={onChangeVeiwMode}
|
||||
>
|
||||
<Button>Format</Button>
|
||||
</Popover>
|
||||
)}
|
||||
{viewModeOptionList.map((option) => (
|
||||
<Select.Option key={option.value}>{option.label}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
defaultValue={order}
|
||||
onChange={handleChangeOrder}
|
||||
>
|
||||
{orderItems.map((item) => (
|
||||
<Select.Option key={item.enum}>{item.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
</Col>
|
||||
{isFormatButtonVisible && (
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="right"
|
||||
content={renderPopoverContent}
|
||||
>
|
||||
<Button>Format</Button>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<Col>
|
||||
<LogControls />
|
||||
</Col>
|
||||
</Row>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
style={defaultSelectStyle}
|
||||
defaultValue={order}
|
||||
onChange={handleChangeOrder}
|
||||
>
|
||||
{orderItems.map((item) => (
|
||||
<Select.Option key={item.enum}>{item.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<LogsTable viewMode={viewMode} linesPerRow={linesPerRow} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Col>
|
||||
<LogControls />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<LogDetailedView />
|
||||
</div>
|
||||
<LogsTable viewMode={viewMode} linesPerRow={linesPerRow} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<LogDetailedView />
|
||||
</div>
|
||||
</PreferenceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import NotFound from 'components/NotFound';
|
||||
import Spinner from 'components/Spinner';
|
||||
import NewDashboard from 'container/NewDashboard';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import { useEffect } from 'react';
|
||||
import { ErrorType } from 'types/common';
|
||||
|
||||
@@ -35,7 +36,11 @@ function DashboardPage(): JSX.Element {
|
||||
return <Spinner tip="Loading.." />;
|
||||
}
|
||||
|
||||
return <NewDashboard />;
|
||||
return (
|
||||
<PreferenceContextProvider>
|
||||
<NewDashboard />
|
||||
</PreferenceContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default DashboardPage;
|
||||
|
||||
@@ -24,7 +24,12 @@ import { getRoutes } from './utils';
|
||||
function SettingsPage(): JSX.Element {
|
||||
const { pathname, search } = useLocation();
|
||||
|
||||
const { user, featureFlags, trialInfo } = useAppContext();
|
||||
const {
|
||||
user,
|
||||
featureFlags,
|
||||
trialInfo,
|
||||
isFetchingActiveLicense,
|
||||
} = useAppContext();
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
const [settingsMenuItems, setSettingsMenuItems] = useState<SidebarItem[]>(
|
||||
@@ -51,6 +56,21 @@ function SettingsPage(): JSX.Element {
|
||||
setSettingsMenuItems((prevItems) => {
|
||||
let updatedItems = [...prevItems];
|
||||
|
||||
if (trialInfo?.workSpaceBlock && !isFetchingActiveLicense) {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
...item,
|
||||
isEnabled: !!(
|
||||
isAdmin &&
|
||||
(item.key === ROUTES.BILLING ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.MY_SETTINGS ||
|
||||
item.key === ROUTES.SHORTCUTS)
|
||||
),
|
||||
}));
|
||||
|
||||
return updatedItems;
|
||||
}
|
||||
|
||||
if (isCloudUser) {
|
||||
if (isAdmin) {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
@@ -61,7 +81,8 @@ function SettingsPage(): JSX.Element {
|
||||
item.key === ROUTES.CUSTOM_DOMAIN_SETTINGS ||
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.INGESTION_SETTINGS ||
|
||||
item.key === ROUTES.ORG_SETTINGS
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.SHORTCUTS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
}));
|
||||
@@ -72,7 +93,8 @@ function SettingsPage(): JSX.Element {
|
||||
...item,
|
||||
isEnabled:
|
||||
item.key === ROUTES.INGESTION_SETTINGS ||
|
||||
item.key === ROUTES.INTEGRATIONS
|
||||
item.key === ROUTES.INTEGRATIONS ||
|
||||
item.key === ROUTES.SHORTCUTS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
}));
|
||||
@@ -87,7 +109,8 @@ function SettingsPage(): JSX.Element {
|
||||
item.key === ROUTES.BILLING ||
|
||||
item.key === ROUTES.INTEGRATIONS ||
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.ORG_SETTINGS
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.SHORTCUTS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
}));
|
||||
@@ -107,7 +130,9 @@ function SettingsPage(): JSX.Element {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
...item,
|
||||
isEnabled:
|
||||
item.key === ROUTES.API_KEYS || item.key === ROUTES.ORG_SETTINGS
|
||||
item.key === ROUTES.API_KEYS ||
|
||||
item.key === ROUTES.ORG_SETTINGS ||
|
||||
item.key === ROUTES.SHORTCUTS
|
||||
? true
|
||||
: item.isEnabled,
|
||||
}));
|
||||
@@ -125,7 +150,15 @@ function SettingsPage(): JSX.Element {
|
||||
|
||||
return updatedItems;
|
||||
});
|
||||
}, [isAdmin, isEditor, isCloudUser, isEnterpriseSelfHostedUser]);
|
||||
}, [
|
||||
isAdmin,
|
||||
isEditor,
|
||||
isCloudUser,
|
||||
isEnterpriseSelfHostedUser,
|
||||
isFetchingActiveLicense,
|
||||
trialInfo?.workSpaceBlock,
|
||||
pathname,
|
||||
]);
|
||||
|
||||
const routes = useMemo(
|
||||
() =>
|
||||
@@ -184,6 +217,13 @@ function SettingsPage(): JSX.Element {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
pathname.startsWith(ROUTES.CHANNELS_EDIT) &&
|
||||
key === ROUTES.ALL_CHANNELS
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return pathname === key;
|
||||
};
|
||||
|
||||
|
||||
@@ -32,7 +32,12 @@ export const getRoutes = (
|
||||
const isEditor = userRole === USER_ROLES.EDITOR;
|
||||
|
||||
if (isWorkspaceBlocked && isAdmin) {
|
||||
settings.push(...organizationSettings(t));
|
||||
settings.push(
|
||||
...organizationSettings(t),
|
||||
...mySettings(t),
|
||||
...billingSettings(t),
|
||||
...keyboardShortcuts(t),
|
||||
);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -67,19 +67,15 @@ export default function TraceDetailsPage(): JSX.Element {
|
||||
key: 'trace-details',
|
||||
children: <TraceDetailsV2 />,
|
||||
},
|
||||
...(process.env.NODE_ENV === 'development'
|
||||
? [
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
<Cone className="funnel-icon" size={16} /> Funnels
|
||||
</div>
|
||||
),
|
||||
key: 'funnels',
|
||||
children: <div />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
<Cone className="funnel-icon" size={16} /> Funnels
|
||||
</div>
|
||||
),
|
||||
key: 'funnels',
|
||||
children: <div />,
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<div className="tab-item">
|
||||
|
||||
@@ -2,6 +2,7 @@ import './DeleteFunnelStep.styles.scss';
|
||||
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { Trash2, X } from 'lucide-react';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
|
||||
interface DeleteFunnelStepProps {
|
||||
isOpen: boolean;
|
||||
@@ -14,8 +15,10 @@ function DeleteFunnelStep({
|
||||
onClose,
|
||||
onStepRemove,
|
||||
}: DeleteFunnelStepProps): JSX.Element {
|
||||
const { handleRunFunnel } = useFunnelContext();
|
||||
const handleStepRemoval = (): void => {
|
||||
onStepRemove();
|
||||
handleRunFunnel();
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import useFunnelConfiguration from 'hooks/TracesFunnels/useFunnelConfiguration';
|
||||
import { PencilLine } from 'lucide-react';
|
||||
import FunnelItemPopover from 'pages/TracesFunnels/components/FunnelsList/FunnelItemPopover';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import CopyToClipboard from 'periscope/components/CopyToClipboard';
|
||||
import { memo, useState } from 'react';
|
||||
import { Span } from 'types/api/trace/getTraceV2';
|
||||
@@ -21,7 +22,6 @@ interface FunnelConfigurationProps {
|
||||
funnel: FunnelData;
|
||||
isTraceDetailsPage?: boolean;
|
||||
span?: Span;
|
||||
disableAutoSave?: boolean;
|
||||
triggerAutoSave?: boolean;
|
||||
showNotifications?: boolean;
|
||||
}
|
||||
@@ -30,15 +30,19 @@ function FunnelConfiguration({
|
||||
funnel,
|
||||
isTraceDetailsPage,
|
||||
span,
|
||||
disableAutoSave,
|
||||
triggerAutoSave,
|
||||
showNotifications,
|
||||
}: FunnelConfigurationProps): JSX.Element {
|
||||
const { isPopoverOpen, setIsPopoverOpen, steps } = useFunnelConfiguration({
|
||||
const { triggerSave } = useFunnelContext();
|
||||
const {
|
||||
isPopoverOpen,
|
||||
setIsPopoverOpen,
|
||||
steps,
|
||||
isSaving,
|
||||
} = useFunnelConfiguration({
|
||||
funnel,
|
||||
disableAutoSave,
|
||||
triggerAutoSave,
|
||||
showNotifications,
|
||||
triggerAutoSave: triggerAutoSave || triggerSave,
|
||||
showNotifications: showNotifications || triggerSave,
|
||||
});
|
||||
const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState<boolean>(
|
||||
false,
|
||||
@@ -106,7 +110,7 @@ function FunnelConfiguration({
|
||||
|
||||
{!isTraceDetailsPage && (
|
||||
<>
|
||||
<StepsFooter stepsCount={steps.length} />
|
||||
<StepsFooter stepsCount={steps.length} isSaving={isSaving || false} />
|
||||
<AddFunnelDescriptionModal
|
||||
isOpen={isDescriptionModalOpen}
|
||||
onClose={handleDescriptionModalClose}
|
||||
@@ -122,7 +126,6 @@ function FunnelConfiguration({
|
||||
FunnelConfiguration.defaultProps = {
|
||||
isTraceDetailsPage: false,
|
||||
span: undefined,
|
||||
disableAutoSave: false,
|
||||
triggerAutoSave: false,
|
||||
showNotifications: false,
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
color: var(--bg-vanilla-400);
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
.step-popover {
|
||||
opacity: 0;
|
||||
width: 22px;
|
||||
|
||||
@@ -40,11 +40,6 @@
|
||||
letter-spacing: 0.12px;
|
||||
border-radius: 2px;
|
||||
|
||||
&--sync {
|
||||
border: 1px solid var(--bg-slate-400);
|
||||
background: var(--bg-ink-300);
|
||||
color: var(--bg-vanilla-400);
|
||||
}
|
||||
&--run {
|
||||
background-color: var(--bg-robin-500);
|
||||
}
|
||||
|
||||
@@ -1,53 +1,14 @@
|
||||
import './StepsFooter.styles.scss';
|
||||
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Skeleton, Spin } from 'antd';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { Cone, Play, RefreshCcw } from 'lucide-react';
|
||||
import { Check, Cone } from 'lucide-react';
|
||||
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import { useMemo } from 'react';
|
||||
import { useIsFetching, useIsMutating } from 'react-query';
|
||||
|
||||
const useFunnelResultsLoading = (): boolean => {
|
||||
const { funnelId } = useFunnelContext();
|
||||
|
||||
const isFetchingFunnelOverview = useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW, funnelId],
|
||||
});
|
||||
|
||||
const isFetchingStepsGraphData = useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA, funnelId],
|
||||
});
|
||||
|
||||
const isFetchingErrorTraces = useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_ERROR_TRACES, funnelId],
|
||||
});
|
||||
|
||||
const isFetchingSlowTraces = useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.GET_FUNNEL_SLOW_TRACES, funnelId],
|
||||
});
|
||||
|
||||
return useMemo(() => {
|
||||
if (!funnelId) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
!!isFetchingFunnelOverview ||
|
||||
!!isFetchingStepsGraphData ||
|
||||
!!isFetchingErrorTraces ||
|
||||
!!isFetchingSlowTraces
|
||||
);
|
||||
}, [
|
||||
funnelId,
|
||||
isFetchingFunnelOverview,
|
||||
isFetchingStepsGraphData,
|
||||
isFetchingErrorTraces,
|
||||
isFetchingSlowTraces,
|
||||
]);
|
||||
};
|
||||
import { useIsMutating } from 'react-query';
|
||||
|
||||
interface StepsFooterProps {
|
||||
stepsCount: number;
|
||||
isSaving: boolean;
|
||||
}
|
||||
|
||||
function ValidTracesCount(): JSX.Element {
|
||||
@@ -93,21 +54,13 @@ function ValidTracesCount(): JSX.Element {
|
||||
return <span className="steps-footer__valid-traces">Valid traces found</span>;
|
||||
}
|
||||
|
||||
function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
|
||||
function StepsFooter({ stepsCount, isSaving }: StepsFooterProps): JSX.Element {
|
||||
const {
|
||||
validTracesCount,
|
||||
handleRunFunnel,
|
||||
hasFunnelBeenExecuted,
|
||||
funnelId,
|
||||
hasIncompleteStepFields,
|
||||
handleSaveFunnel,
|
||||
hasUnsavedChanges,
|
||||
} = useFunnelContext();
|
||||
|
||||
const isFunnelResultsLoading = useFunnelResultsLoading();
|
||||
|
||||
const isFunnelUpdateMutating = useIsMutating([
|
||||
REACT_QUERY_KEY.UPDATE_FUNNEL_STEPS,
|
||||
funnelId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="steps-footer">
|
||||
<div className="steps-footer__left">
|
||||
@@ -117,38 +70,16 @@ function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
|
||||
<ValidTracesCount />
|
||||
</div>
|
||||
<div className="steps-footer__right">
|
||||
{!!isFunnelUpdateMutating && (
|
||||
<div className="steps-footer__button steps-footer__button--updating">
|
||||
<Spin
|
||||
indicator={<LoadingOutlined style={{ color: 'grey' }} />}
|
||||
size="small"
|
||||
/>
|
||||
Updating
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasFunnelBeenExecuted ? (
|
||||
<Button
|
||||
disabled={validTracesCount === 0}
|
||||
onClick={handleRunFunnel}
|
||||
type="primary"
|
||||
className="steps-footer__button steps-footer__button--run"
|
||||
icon={<Play size={16} />}
|
||||
>
|
||||
Run funnel
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="text"
|
||||
className="steps-footer__button steps-footer__button--sync"
|
||||
icon={<RefreshCcw size={16} />}
|
||||
onClick={handleRunFunnel}
|
||||
loading={isFunnelResultsLoading}
|
||||
disabled={validTracesCount === 0}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={hasIncompleteStepFields || !hasUnsavedChanges}
|
||||
onClick={handleSaveFunnel}
|
||||
type="primary"
|
||||
className="steps-footer__button steps-footer__button--run"
|
||||
icon={<Check size={14} />}
|
||||
loading={isSaving}
|
||||
>
|
||||
Save funnel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -29,13 +29,20 @@ Chart.register(
|
||||
);
|
||||
|
||||
function FunnelGraph(): JSX.Element {
|
||||
const { funnelId } = useFunnelContext();
|
||||
const { funnelId, startTime, endTime, steps } = useFunnelContext();
|
||||
|
||||
const payload = {
|
||||
start_time: startTime,
|
||||
end_time: endTime,
|
||||
steps,
|
||||
};
|
||||
|
||||
const {
|
||||
data: stepsData,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
} = useFunnelStepsGraphData(funnelId);
|
||||
} = useFunnelStepsGraphData(funnelId, payload);
|
||||
|
||||
const data = useMemo(() => stepsData?.payload?.data?.[0]?.data, [
|
||||
stepsData?.payload?.data,
|
||||
|
||||
@@ -16,7 +16,6 @@ function FunnelResults(): JSX.Element {
|
||||
isValidateStepsLoading,
|
||||
hasIncompleteStepFields,
|
||||
hasAllEmptyStepFields,
|
||||
hasFunnelBeenExecuted,
|
||||
funnelId,
|
||||
} = useFunnelContext();
|
||||
|
||||
@@ -47,14 +46,6 @@ function FunnelResults(): JSX.Element {
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (!hasFunnelBeenExecuted) {
|
||||
return (
|
||||
<EmptyFunnelResults
|
||||
title="Funnel has not been run yet."
|
||||
description="Run the funnel to see the results"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="funnel-results">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
|
||||
import { useMemo } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
import FunnelTable from './FunnelTable';
|
||||
import { topTracesTableColumns } from './utils';
|
||||
@@ -24,6 +25,7 @@ interface FunnelTopTracesTableProps {
|
||||
SuccessResponse<SlowTraceData | ErrorTraceData> | ErrorResponse,
|
||||
Error
|
||||
>;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
function FunnelTopTracesTable({
|
||||
@@ -32,6 +34,7 @@ function FunnelTopTracesTable({
|
||||
stepBOrder,
|
||||
title,
|
||||
tooltip,
|
||||
steps,
|
||||
useQueryHook,
|
||||
}: FunnelTopTracesTableProps): JSX.Element {
|
||||
const { startTime, endTime } = useFunnelContext();
|
||||
@@ -41,8 +44,9 @@ function FunnelTopTracesTable({
|
||||
end_time: endTime,
|
||||
step_start: stepAOrder,
|
||||
step_end: stepBOrder,
|
||||
steps,
|
||||
}),
|
||||
[startTime, endTime, stepAOrder, stepBOrder],
|
||||
[startTime, endTime, stepAOrder, stepBOrder, steps],
|
||||
);
|
||||
|
||||
const { data: response, isLoading, isFetching } = useQueryHook(
|
||||
|
||||
@@ -6,7 +6,7 @@ import FunnelMetricsTable from './FunnelMetricsTable';
|
||||
function OverallMetrics(): JSX.Element {
|
||||
const { funnelId } = useParams<{ funnelId: string }>();
|
||||
const { isLoading, metricsData, conversionRate, isError } = useFunnelMetrics({
|
||||
funnelId: funnelId || '',
|
||||
funnelId,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -52,11 +52,13 @@ function StepsTransitionResults(): JSX.Element {
|
||||
funnelId={funnelId}
|
||||
stepAOrder={stepAOrder}
|
||||
stepBOrder={stepBOrder}
|
||||
steps={steps}
|
||||
/>
|
||||
<TopTracesWithErrors
|
||||
funnelId={funnelId}
|
||||
stepAOrder={stepAOrder}
|
||||
stepBOrder={stepBOrder}
|
||||
steps={steps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useFunnelSlowTraces } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
import FunnelTopTracesTable from './FunnelTopTracesTable';
|
||||
|
||||
@@ -6,6 +7,7 @@ interface TopSlowestTracesProps {
|
||||
funnelId: string;
|
||||
stepAOrder: number;
|
||||
stepBOrder: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
function TopSlowestTraces(props: TopSlowestTracesProps): JSX.Element {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useFunnelErrorTraces } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
import FunnelTopTracesTable from './FunnelTopTracesTable';
|
||||
|
||||
@@ -6,6 +7,7 @@ interface TopTracesWithErrorsProps {
|
||||
funnelId: string;
|
||||
stepAOrder: number;
|
||||
stepBOrder: number;
|
||||
steps: FunnelStepData[];
|
||||
}
|
||||
|
||||
function TopTracesWithErrors(props: TopTracesWithErrorsProps): JSX.Element {
|
||||
|
||||
@@ -18,10 +18,4 @@ export const topTracesTableColumns = [
|
||||
key: 'duration_ms',
|
||||
render: (value: string): string => getYAxisFormattedValue(value, 'ms'),
|
||||
},
|
||||
{
|
||||
title: 'SPAN COUNT',
|
||||
dataIndex: 'span_count',
|
||||
key: 'span_count',
|
||||
render: (value: number): string => value.toString(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -14,8 +14,6 @@ export const initialStepsData: FunnelStepData[] = [
|
||||
latency_pointer: 'start',
|
||||
latency_type: undefined,
|
||||
has_errors: false,
|
||||
name: '',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
id: v4(),
|
||||
@@ -29,8 +27,6 @@ export const initialStepsData: FunnelStepData[] = [
|
||||
latency_pointer: 'start',
|
||||
latency_type: LatencyOptions.P95,
|
||||
has_errors: false,
|
||||
name: '',
|
||||
description: '',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { ValidateFunnelResponse } from 'api/traceFunnels';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time as TimeV2,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { normalizeSteps } from 'hooks/TracesFunnels/useFunnelConfiguration';
|
||||
import { useValidateFunnelSteps } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { initialStepsData } from 'pages/TracesFunnelDetails/constants';
|
||||
import {
|
||||
createContext,
|
||||
@@ -41,6 +41,9 @@ interface FunnelContextType {
|
||||
handleStepChange: (index: number, newStep: Partial<FunnelStepData>) => void;
|
||||
handleStepRemoval: (index: number) => void;
|
||||
handleRunFunnel: () => void;
|
||||
handleSaveFunnel: () => void;
|
||||
triggerSave: boolean;
|
||||
hasUnsavedChanges: boolean;
|
||||
validationResponse:
|
||||
| SuccessResponse<ValidateFunnelResponse>
|
||||
| ErrorResponse
|
||||
@@ -54,8 +57,10 @@ interface FunnelContextType {
|
||||
spanName: string,
|
||||
) => void;
|
||||
handleRestoreSteps: (oldSteps: FunnelStepData[]) => void;
|
||||
hasFunnelBeenExecuted: boolean;
|
||||
setHasFunnelBeenExecuted: Dispatch<SetStateAction<boolean>>;
|
||||
isUpdatingFunnel: boolean;
|
||||
setIsUpdatingFunnel: Dispatch<SetStateAction<boolean>>;
|
||||
lastUpdatedSteps: FunnelStepData[];
|
||||
setLastUpdatedSteps: Dispatch<SetStateAction<FunnelStepData[]>>;
|
||||
}
|
||||
|
||||
const FunnelContext = createContext<FunnelContextType | undefined>(undefined);
|
||||
@@ -86,6 +91,19 @@ export function FunnelProvider({
|
||||
const funnel = data?.payload;
|
||||
const initialSteps = funnel?.steps?.length ? funnel.steps : initialStepsData;
|
||||
const [steps, setSteps] = useState<FunnelStepData[]>(initialSteps);
|
||||
const [triggerSave, setTriggerSave] = useState<boolean>(false);
|
||||
const [isUpdatingFunnel, setIsUpdatingFunnel] = useState<boolean>(false);
|
||||
const [lastUpdatedSteps, setLastUpdatedSteps] = useState<FunnelStepData[]>(
|
||||
initialSteps,
|
||||
);
|
||||
|
||||
// Check if there are unsaved changes by comparing with initial steps from API
|
||||
const hasUnsavedChanges = useMemo(() => {
|
||||
const normalizedCurrentSteps = normalizeSteps(steps);
|
||||
const normalizedInitialSteps = normalizeSteps(lastUpdatedSteps);
|
||||
return !isEqual(normalizedCurrentSteps, normalizedInitialSteps);
|
||||
}, [steps, lastUpdatedSteps]);
|
||||
|
||||
const { hasIncompleteStepFields, hasAllEmptyStepFields } = useMemo(
|
||||
() => ({
|
||||
hasAllEmptyStepFields: steps.every(
|
||||
@@ -98,15 +116,6 @@ export function FunnelProvider({
|
||||
[steps],
|
||||
);
|
||||
|
||||
const [unexecutedFunnels, setUnexecutedFunnels] = useLocalStorage<string[]>(
|
||||
LOCALSTORAGE.UNEXECUTED_FUNNELS,
|
||||
[],
|
||||
);
|
||||
|
||||
const [hasFunnelBeenExecuted, setHasFunnelBeenExecuted] = useState(
|
||||
!unexecutedFunnels.includes(funnelId),
|
||||
);
|
||||
|
||||
const {
|
||||
data: validationResponse,
|
||||
isLoading: isValidationLoading,
|
||||
@@ -116,7 +125,13 @@ export function FunnelProvider({
|
||||
selectedTime,
|
||||
startTime,
|
||||
endTime,
|
||||
enabled: !!funnelId && !!selectedTime && !!startTime && !!endTime,
|
||||
enabled:
|
||||
!!funnelId &&
|
||||
!!selectedTime &&
|
||||
!!startTime &&
|
||||
!!endTime &&
|
||||
!hasIncompleteStepFields,
|
||||
steps,
|
||||
});
|
||||
|
||||
const validTracesCount = useMemo(
|
||||
@@ -185,11 +200,7 @@ export function FunnelProvider({
|
||||
|
||||
const handleRunFunnel = useCallback(async (): Promise<void> => {
|
||||
if (validTracesCount === 0) return;
|
||||
if (!hasFunnelBeenExecuted) {
|
||||
setUnexecutedFunnels(unexecutedFunnels.filter((id) => id !== funnelId));
|
||||
|
||||
setHasFunnelBeenExecuted(true);
|
||||
}
|
||||
queryClient.refetchQueries([
|
||||
REACT_QUERY_KEY.GET_FUNNEL_OVERVIEW,
|
||||
funnelId,
|
||||
@@ -215,15 +226,13 @@ export function FunnelProvider({
|
||||
funnelId,
|
||||
selectedTime,
|
||||
]);
|
||||
}, [
|
||||
funnelId,
|
||||
hasFunnelBeenExecuted,
|
||||
unexecutedFunnels,
|
||||
queryClient,
|
||||
selectedTime,
|
||||
setUnexecutedFunnels,
|
||||
validTracesCount,
|
||||
]);
|
||||
}, [funnelId, queryClient, selectedTime, validTracesCount]);
|
||||
|
||||
const handleSaveFunnel = useCallback(() => {
|
||||
setTriggerSave(true);
|
||||
// Reset the trigger after a brief moment to allow useFunnelConfiguration to pick it up
|
||||
setTimeout(() => setTriggerSave(false), 100);
|
||||
}, []);
|
||||
|
||||
const value = useMemo<FunnelContextType>(
|
||||
() => ({
|
||||
@@ -239,14 +248,19 @@ export function FunnelProvider({
|
||||
handleAddStep: addNewStep,
|
||||
handleStepRemoval,
|
||||
handleRunFunnel,
|
||||
handleSaveFunnel,
|
||||
triggerSave,
|
||||
validationResponse,
|
||||
isValidateStepsLoading: isValidationLoading || isValidationFetching,
|
||||
hasIncompleteStepFields,
|
||||
hasAllEmptyStepFields,
|
||||
handleReplaceStep,
|
||||
handleRestoreSteps,
|
||||
hasFunnelBeenExecuted,
|
||||
setHasFunnelBeenExecuted,
|
||||
hasUnsavedChanges,
|
||||
setIsUpdatingFunnel,
|
||||
isUpdatingFunnel,
|
||||
lastUpdatedSteps,
|
||||
setLastUpdatedSteps,
|
||||
}),
|
||||
[
|
||||
funnelId,
|
||||
@@ -260,6 +274,8 @@ export function FunnelProvider({
|
||||
addNewStep,
|
||||
handleStepRemoval,
|
||||
handleRunFunnel,
|
||||
handleSaveFunnel,
|
||||
triggerSave,
|
||||
validationResponse,
|
||||
isValidationLoading,
|
||||
isValidationFetching,
|
||||
@@ -267,8 +283,11 @@ export function FunnelProvider({
|
||||
hasAllEmptyStepFields,
|
||||
handleReplaceStep,
|
||||
handleRestoreSteps,
|
||||
hasFunnelBeenExecuted,
|
||||
setHasFunnelBeenExecuted,
|
||||
hasUnsavedChanges,
|
||||
setIsUpdatingFunnel,
|
||||
isUpdatingFunnel,
|
||||
lastUpdatedSteps,
|
||||
setLastUpdatedSteps,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ import { Input } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { AxiosError } from 'axios';
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useCreateFunnel } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Check, X } from 'lucide-react';
|
||||
@@ -34,11 +32,6 @@ function CreateFunnel({
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const [unexecutedFunnels, setUnexecutedFunnels] = useLocalStorage<string[]>(
|
||||
LOCALSTORAGE.UNEXECUTED_FUNNELS,
|
||||
[],
|
||||
);
|
||||
|
||||
const handleCreate = (): void => {
|
||||
createFunnelMutation.mutate(
|
||||
{
|
||||
@@ -61,9 +54,6 @@ function CreateFunnel({
|
||||
queryClient.invalidateQueries([REACT_QUERY_KEY.GET_FUNNELS_LIST]);
|
||||
|
||||
const funnelId = data?.payload?.funnel_id;
|
||||
if (funnelId) {
|
||||
setUnexecutedFunnels([...unexecutedFunnels, funnelId]);
|
||||
}
|
||||
|
||||
onClose(funnelId);
|
||||
if (funnelId && redirectToDetails) {
|
||||
|
||||
@@ -2,13 +2,16 @@ import '../RenameFunnel/RenameFunnel.styles.scss';
|
||||
import './DeleteFunnel.styles.scss';
|
||||
|
||||
import SignozModal from 'components/SignozModal/SignozModal';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useDeleteFunnel } from 'hooks/TracesFunnels/useFunnels';
|
||||
import { useLocalStorage } from 'hooks/useLocalStorage';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Trash2, X } from 'lucide-react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { FunnelStepData } from 'types/api/traceFunnels';
|
||||
|
||||
interface DeleteFunnelProps {
|
||||
isOpen: boolean;
|
||||
@@ -29,6 +32,13 @@ function DeleteFunnel({
|
||||
|
||||
const history = useHistory();
|
||||
const { pathname } = history.location;
|
||||
|
||||
// localStorage hook for funnel steps
|
||||
const localStorageKey = `${LOCALSTORAGE.FUNNEL_STEPS}_${funnelId}`;
|
||||
const [, , clearLocalStorageSavedSteps] = useLocalStorage<
|
||||
FunnelStepData[] | null
|
||||
>(localStorageKey, null);
|
||||
|
||||
const handleDelete = (): void => {
|
||||
deleteFunnelMutation.mutate(
|
||||
{
|
||||
@@ -39,6 +49,7 @@ function DeleteFunnel({
|
||||
notifications.success({
|
||||
message: 'Funnel deleted successfully',
|
||||
});
|
||||
clearLocalStorageSavedSteps();
|
||||
onClose();
|
||||
|
||||
if (
|
||||
|
||||
@@ -14,8 +14,7 @@ function TracesModulePage(): JSX.Element {
|
||||
|
||||
const routes: TabRoutes[] = [
|
||||
tracesExplorer,
|
||||
// TODO(shaheer): remove this check after everything is ready
|
||||
process.env.NODE_ENV === 'development' ? tracesFunnel(pathname) : null,
|
||||
tracesFunnel(pathname),
|
||||
tracesSaveView,
|
||||
].filter(Boolean) as TabRoutes[];
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||
import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags';
|
||||
import {
|
||||
LicensePlatform,
|
||||
@@ -58,6 +59,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
(): boolean => getLocalStorageApi(LOCALSTORAGE.IS_LOGGED_IN) === 'true',
|
||||
);
|
||||
const [org, setOrg] = useState<Organization[] | null>(null);
|
||||
const [changelog, setChangelog] = useState<ChangelogSchema | null>(null);
|
||||
|
||||
// if the user.id is not present, for migration older cases then we need to logout only for current logged in users!
|
||||
useEffect(() => {
|
||||
@@ -253,6 +255,13 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
[org],
|
||||
);
|
||||
|
||||
const updateChangelog = useCallback(
|
||||
(payload: ChangelogSchema): void => {
|
||||
setChangelog(payload);
|
||||
},
|
||||
[setChangelog],
|
||||
);
|
||||
|
||||
// global event listener for AFTER_LOGIN event to start the user fetch post all actions are complete
|
||||
useGlobalEventListener('AFTER_LOGIN', (event) => {
|
||||
if (event.detail) {
|
||||
@@ -296,11 +305,13 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
featureFlagsFetchError,
|
||||
orgPreferencesFetchError,
|
||||
activeLicense,
|
||||
changelog,
|
||||
activeLicenseRefetch,
|
||||
updateUser,
|
||||
updateOrgPreferences,
|
||||
updateUserPreferenceInContext,
|
||||
updateOrg,
|
||||
updateChangelog,
|
||||
versionData: versionData?.payload || null,
|
||||
}),
|
||||
[
|
||||
@@ -319,8 +330,10 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
|
||||
orgPreferences,
|
||||
activeLicenseRefetch,
|
||||
orgPreferencesFetchError,
|
||||
changelog,
|
||||
updateUserPreferenceInContext,
|
||||
updateOrg,
|
||||
updateChangelog,
|
||||
user,
|
||||
userFetchError,
|
||||
versionData,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ChangelogSchema } from 'types/api/changelog/getChangelogByVersion';
|
||||
import APIError from 'types/api/error';
|
||||
import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags';
|
||||
import { LicenseResModel, TrialInfo } from 'types/api/licensesV3/getActive';
|
||||
@@ -26,11 +27,13 @@ export interface IAppContext {
|
||||
activeLicenseFetchError: APIError | null;
|
||||
featureFlagsFetchError: unknown;
|
||||
orgPreferencesFetchError: unknown;
|
||||
changelog: ChangelogSchema | null;
|
||||
activeLicenseRefetch: () => void;
|
||||
updateUser: (user: IUser) => void;
|
||||
updateOrgPreferences: (orgPreferences: OrgPreference[]) => void;
|
||||
updateUserPreferenceInContext: (userPreference: UserPreference) => void;
|
||||
updateOrg(orgId: string, updatedOrgName: string): void;
|
||||
updateChangelog(payload: ChangelogSchema): void;
|
||||
versionData: PayloadProps | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ export function getAppContextMock(
|
||||
},
|
||||
isFetchingActiveLicense: false,
|
||||
activeLicenseFetchError: null,
|
||||
changelog: null,
|
||||
user: {
|
||||
accessJwt: 'some-token',
|
||||
refreshJwt: 'some-refresh-token',
|
||||
@@ -236,6 +237,7 @@ export function getAppContextMock(
|
||||
updateOrg: jest.fn(),
|
||||
updateOrgPreferences: jest.fn(),
|
||||
activeLicenseRefetch: jest.fn(),
|
||||
updateChangelog: jest.fn(),
|
||||
versionData: {
|
||||
version: '1.0.0',
|
||||
ee: 'Y',
|
||||
|
||||
39
frontend/src/types/api/changelog/getChangelogByVersion.ts
Normal file
39
frontend/src/types/api/changelog/getChangelogByVersion.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export type Media = {
|
||||
id: number;
|
||||
documentId: string;
|
||||
ext: string;
|
||||
url: string;
|
||||
mime: string;
|
||||
alternativeText: string | null;
|
||||
[key: string]: any; // Allow other fields (e.g., mime, size) to be flexible
|
||||
};
|
||||
|
||||
type Feature = {
|
||||
id: number;
|
||||
documentId: string;
|
||||
title: string;
|
||||
sort_order: number | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
publishedAt: string;
|
||||
description: string;
|
||||
deployment_type: string | null;
|
||||
media: Media | null;
|
||||
};
|
||||
|
||||
export interface ChangelogSchema {
|
||||
id: number;
|
||||
documentId: string;
|
||||
version: string;
|
||||
release_date: string;
|
||||
bug_fixes: string | null;
|
||||
maintenance: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
publishedAt: string;
|
||||
features: Feature[];
|
||||
}
|
||||
|
||||
export const SupportedImageTypes = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
|
||||
export const SupportedVideoTypes = ['.mp4', '.webm'];
|
||||
@@ -105,7 +105,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
TRACES_FUNNELS_DETAIL: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
API_KEYS: ['ADMIN'],
|
||||
CUSTOM_DOMAIN_SETTINGS: ['ADMIN'],
|
||||
LOGS_BASE: [],
|
||||
LOGS_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
OLD_LOGS_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
SHORTCUTS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
INTEGRATIONS: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
@@ -120,4 +120,7 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
|
||||
API_MONITORING: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
WORKSPACE_ACCESS_RESTRICTED: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
METRICS_EXPLORER_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
INFRASTRUCTURE_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
API_MONITORING_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
MESSAGING_QUEUES_BASE: ['ADMIN', 'EDITOR', 'VIEWER'],
|
||||
};
|
||||
|
||||
248
go.mod
248
go.mod
@@ -8,41 +8,41 @@ require (
|
||||
github.com/ClickHouse/clickhouse-go/v2 v2.30.0
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.39
|
||||
github.com/SigNoz/signoz-otel-collector v0.111.43-bump-a2ff26b
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/coreos/go-oidc/v3 v3.11.0
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/go-co-op/gocron v1.30.1
|
||||
github.com/go-openapi/runtime v0.28.0
|
||||
github.com/go-openapi/strfmt v0.23.0
|
||||
github.com/go-redis/redis/v8 v8.11.5
|
||||
github.com/go-redis/redismock/v8 v8.11.5
|
||||
github.com/go-viper/mapstructure/v2 v2.1.0
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/gojek/heimdall/v7 v7.0.3
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/handlers v1.5.1
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/huandu/go-sqlbuilder v1.35.0
|
||||
github.com/jackc/pgx/v5 v5.7.2
|
||||
github.com/jmoiron/sqlx v1.3.4
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/knadh/koanf v1.5.0
|
||||
github.com/knadh/koanf/v2 v2.1.1
|
||||
github.com/knadh/koanf/v2 v2.2.0
|
||||
github.com/mailru/easyjson v0.7.7
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/open-telemetry/opamp-go v0.5.0
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.111.0
|
||||
github.com/open-telemetry/opamp-go v0.19.0
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/stanza v0.128.0
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/alertmanager v0.28.0
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/prometheus/common v0.61.0
|
||||
github.com/prometheus/prometheus v0.300.1
|
||||
github.com/prometheus/alertmanager v0.28.1
|
||||
github.com/prometheus/client_golang v1.22.0
|
||||
github.com/prometheus/common v0.64.0
|
||||
github.com/prometheus/prometheus v0.304.1
|
||||
github.com/rs/cors v1.11.1
|
||||
github.com/russellhaering/gosaml2 v0.9.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
@@ -57,47 +57,48 @@ require (
|
||||
github.com/uptrace/bun v1.2.9
|
||||
github.com/uptrace/bun/dialect/pgdialect v1.2.9
|
||||
github.com/uptrace/bun/dialect/sqlitedialect v1.2.9
|
||||
go.opentelemetry.io/collector/confmap v1.17.0
|
||||
go.opentelemetry.io/collector/pdata v1.17.0
|
||||
go.opentelemetry.io/collector/processor v0.111.0
|
||||
go.opentelemetry.io/collector/confmap v1.34.0
|
||||
go.opentelemetry.io/collector/otelcol v0.128.0
|
||||
go.opentelemetry.io/collector/pdata v1.34.0
|
||||
go.opentelemetry.io/contrib/config v0.10.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0
|
||||
go.opentelemetry.io/otel v1.34.0
|
||||
go.opentelemetry.io/otel/metric v1.34.0
|
||||
go.opentelemetry.io/otel/sdk v1.34.0
|
||||
go.opentelemetry.io/otel/trace v1.34.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0
|
||||
go.opentelemetry.io/otel v1.36.0
|
||||
go.opentelemetry.io/otel/metric v1.36.0
|
||||
go.opentelemetry.io/otel/sdk v1.36.0
|
||||
go.opentelemetry.io/otel/trace v1.36.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
||||
golang.org/x/oauth2 v0.24.0
|
||||
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.14.0
|
||||
golang.org/x/text v0.25.0
|
||||
google.golang.org/protobuf v1.36.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/apimachinery v0.31.3
|
||||
k8s.io/apimachinery v0.32.3
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/auth v0.13.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.6.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||
cloud.google.com/go/auth v0.16.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.7.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
|
||||
github.com/ClickHouse/ch-go v0.63.1 // indirect
|
||||
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.5 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.7 // indirect
|
||||
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/coder/quartz v0.1.2 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
@@ -107,10 +108,10 @@ require (
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/edsrzf/mmap-go v1.2.0 // indirect
|
||||
github.com/elastic/lunes v0.1.0 // indirect
|
||||
github.com/expr-lang/expr v1.17.0 // indirect
|
||||
github.com/expr-lang/expr v1.17.5 // indirect
|
||||
github.com/facette/natsort v0.0.0-20181210072756-2cd4dd1e2dcb // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-faster/city v1.0.1 // indirect
|
||||
github.com/go-faster/errors v0.7.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
@@ -125,20 +126,20 @@ require (
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-openapi/validate v0.24.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/gofrs/uuid v4.4.0+incompatible // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/gojek/valkyrie v0.0.0-20180215180059-6aee720afcdf // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/s2a-go v0.1.9 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
|
||||
github.com/gopherjs/gopherjs v1.17.2 // indirect
|
||||
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-msgpack/v2 v2.1.1 // indirect
|
||||
@@ -156,21 +157,21 @@ require (
|
||||
github.com/jessevdk/go-flags v1.6.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/jpillora/backoff v1.0.0 // indirect
|
||||
github.com/jtolds/gls v4.20.0+incompatible // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-syslog/v4 v4.2.0 // indirect
|
||||
github.com/leodido/ragel-machinery v0.0.0-20190525184631-5f46317e436b // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240408141607-282e7b5d6b74 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/magefile/mage v1.15.0 // indirect
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/mdlayher/vsock v1.2.1 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/miekg/dns v1.1.65 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
@@ -180,33 +181,38 @@ require (
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.111.0 // indirect
|
||||
github.com/oklog/ulid/v2 v2.1.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.128.0 // indirect
|
||||
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.128.0 // indirect
|
||||
github.com/paulmach/orb v0.11.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common/sigv4 v0.1.0 // indirect
|
||||
github.com/prometheus/exporter-toolkit v0.13.2 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/exporter-toolkit v0.14.0 // indirect
|
||||
github.com/prometheus/otlptranslator v0.0.0-20250320144820-d800c8b0eb07 // indirect
|
||||
github.com/prometheus/procfs v0.16.1 // indirect
|
||||
github.com/prometheus/sigv4 v0.1.2 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/segmentio/backo-go v1.0.1 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.24.9 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.5 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/shurcooL/httpfs v0.0.0-20230704072500-f1e31cf0ba5c // indirect
|
||||
github.com/shurcooL/vfsgen v0.0.0-20230704071429-0000e147ea92 // indirect
|
||||
github.com/smarty/assertions v1.15.0 // indirect
|
||||
github.com/spf13/cobra v1.8.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/trivago/tgo v1.0.7 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
@@ -216,65 +222,79 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/collector v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/component v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/component/componentprofiles v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/component/componentstatus v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configtelemetry v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/converter/expandconverter v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.17.0 // indirect
|
||||
go.opentelemetry.io/collector/connector v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/connector/connectorprofiles v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/consumerprofiles v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/consumertest v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter/exporterprofiles v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/extension v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/experimental/storage v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/extensioncapabilities v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/featuregate v1.17.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/globalgates v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/globalsignal v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/otelcol v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/pprofile v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/testdata v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/pipeline v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/processorprofiles v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/receiverprofiles v0.111.0 // indirect
|
||||
go.opentelemetry.io/collector/semconv v0.116.0 // indirect
|
||||
go.opentelemetry.io/collector/service v0.111.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.6.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.52.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.6.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.10.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.10.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 // indirect
|
||||
go.opentelemetry.io/collector/component v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/component/componentstatus v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/component/componenttest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/config/configtelemetry v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/provider/envprovider v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/provider/fileprovider v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/confmap/xconfmap v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/connector v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/connector/connectortest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/connector/xconnector v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/consumererror v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/consumertest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/consumer/xconsumer v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter/exportertest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/exporter/xexporter v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/extension v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/extensioncapabilities v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/extensiontest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/extension/xextension v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/featuregate v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/fanoutconsumer v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/internal/telemetry v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/pprofile v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/pdata/testdata v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/pipeline v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/pipeline/xpipeline v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/processor v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/processorhelper v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/processortest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/processor/xprocessor v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver v1.34.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/receiverhelper v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/receivertest v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/receiver/xreceiver v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/semconv v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/service v0.128.0 // indirect
|
||||
go.opentelemetry.io/collector/service/hostcapabilities v0.128.0 // indirect
|
||||
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.60.0 // indirect
|
||||
go.opentelemetry.io/contrib/otelconf v0.16.0 // indirect
|
||||
go.opentelemetry.io/contrib/propagators/b3 v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/prometheus v0.58.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect
|
||||
go.opentelemetry.io/otel/log v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/sdk/log v0.12.2 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/goleak v1.3.0 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
gonum.org/v1/gonum v0.15.1 // indirect
|
||||
google.golang.org/api v0.213.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241216192217-9240e9c98484 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/grpc v1.69.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.33.0 // indirect
|
||||
gonum.org/v1/gonum v0.16.0 // indirect
|
||||
google.golang.org/api v0.236.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
gopkg.in/telebot.v3 v3.3.8 // indirect
|
||||
k8s.io/client-go v0.31.3 // indirect
|
||||
k8s.io/client-go v0.32.3 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
|
||||
k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
|
||||
sigs.k8s.io/yaml v1.4.0 // indirect
|
||||
)
|
||||
|
||||
@@ -75,17 +75,15 @@ comparison
|
||||
| key NOT CONTAINS value
|
||||
;
|
||||
|
||||
// in(...) or in[...] - now also supports variables
|
||||
// in(...) or in[...]
|
||||
inClause
|
||||
: IN LPAREN valueList RPAREN
|
||||
| IN LBRACK valueList RBRACK
|
||||
| IN variable // NEW: support for IN $var, IN {{var}}, IN [[var]]
|
||||
;
|
||||
|
||||
notInClause
|
||||
: NOT IN LPAREN valueList RPAREN
|
||||
| NOT IN LBRACK valueList RBRACK
|
||||
| NOT IN variable // NEW: support for NOT IN $var, etc.
|
||||
;
|
||||
|
||||
// List of values for in(...) or in[...]
|
||||
@@ -128,21 +126,13 @@ array
|
||||
|
||||
/*
|
||||
* A 'value' can be a string literal (double or single-quoted),
|
||||
// a numeric literal, boolean, a "bare" token, or a variable.
|
||||
// a numeric literal, boolean, or a "bare" token as needed.
|
||||
*/
|
||||
value
|
||||
: QUOTED_TEXT
|
||||
| NUMBER
|
||||
| BOOL
|
||||
| KEY
|
||||
| variable // NEW: variables can be used as values
|
||||
;
|
||||
|
||||
// NEW: Variable rule to support different variable syntaxes
|
||||
variable
|
||||
: DOLLAR_VAR
|
||||
| CURLY_VAR
|
||||
| SQUARE_VAR
|
||||
;
|
||||
|
||||
/*
|
||||
@@ -200,11 +190,6 @@ BOOL
|
||||
| [Ff][Aa][Ll][Ss][Ee]
|
||||
;
|
||||
|
||||
// NEW: Variable token types
|
||||
DOLLAR_VAR : '$' [a-zA-Z_] [a-zA-Z0-9._]* ;
|
||||
CURLY_VAR : '{{' [ \t]* '.'? [a-zA-Z_] [a-zA-Z0-9._]* [ \t]* '}}' ;
|
||||
SQUARE_VAR : '[[' [ \t]* '.'? [a-zA-Z_] [a-zA-Z0-9._]* [ \t]* ']]' ;
|
||||
|
||||
fragment SIGN : [+-] ;
|
||||
|
||||
// Numbers: optional sign, then digits, optional fractional part,
|
||||
|
||||
@@ -12,4 +12,16 @@ type Analytics interface {
|
||||
|
||||
// Sends analytics messages to an analytics backend.
|
||||
Send(context.Context, ...analyticstypes.Message)
|
||||
|
||||
// Tracks an event on a group level. Input is group, event name, and attributes. The user is "stats_<org_id>".
|
||||
TrackGroup(context.Context, string, string, map[string]any)
|
||||
|
||||
// Tracks an event on a user level and attributes it with the group. Input is group, user, event name, and attributes.
|
||||
TrackUser(context.Context, string, string, string, map[string]any)
|
||||
|
||||
// Identifies a group. Input is group, traits.
|
||||
IdentifyGroup(context.Context, string, map[string]any)
|
||||
|
||||
// Identifies a user. Input is group, user, traits.
|
||||
IdentifyUser(context.Context, string, string, map[string]any)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,18 @@ func (provider *Provider) Start(_ context.Context) error {
|
||||
|
||||
func (provider *Provider) Send(ctx context.Context, messages ...analyticstypes.Message) {}
|
||||
|
||||
func (provider *Provider) TrackGroup(ctx context.Context, group, event string, attributes map[string]any) {
|
||||
}
|
||||
|
||||
func (provider *Provider) TrackUser(ctx context.Context, group, user, event string, attributes map[string]any) {
|
||||
}
|
||||
|
||||
func (provider *Provider) IdentifyGroup(ctx context.Context, group string, traits map[string]any) {
|
||||
}
|
||||
|
||||
func (provider *Provider) IdentifyUser(ctx context.Context, group, user string, traits map[string]any) {
|
||||
}
|
||||
|
||||
func (provider *Provider) Stop(_ context.Context) error {
|
||||
close(provider.stopC)
|
||||
return nil
|
||||
|
||||
@@ -27,7 +27,25 @@ func (provider *provider) Start(_ context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *provider) Send(ctx context.Context, messages ...analyticstypes.Message) {}
|
||||
func (provider *provider) Send(ctx context.Context, messages ...analyticstypes.Message) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) TrackGroup(ctx context.Context, group, event string, attributes map[string]any) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) TrackUser(ctx context.Context, group, user, event string, attributes map[string]any) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) IdentifyGroup(ctx context.Context, group string, traits map[string]any) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) IdentifyUser(ctx context.Context, group, user string, traits map[string]any) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(_ context.Context) error {
|
||||
close(provider.stopC)
|
||||
|
||||
@@ -50,6 +50,100 @@ func (provider *provider) Send(ctx context.Context, messages ...analyticstypes.M
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) TrackGroup(ctx context.Context, group, event string, properties map[string]any) {
|
||||
if properties == nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping event", "group", group, "event", event)
|
||||
return
|
||||
}
|
||||
|
||||
err := provider.client.Enqueue(analyticstypes.Track{
|
||||
UserId: "stats_" + group,
|
||||
Event: event,
|
||||
Properties: analyticstypes.NewPropertiesFromMap(properties),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: group,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) TrackUser(ctx context.Context, group, user, event string, properties map[string]any) {
|
||||
if properties == nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping event", "user", user, "group", group, "event", event)
|
||||
return
|
||||
}
|
||||
|
||||
err := provider.client.Enqueue(analyticstypes.Track{
|
||||
UserId: user,
|
||||
Event: event,
|
||||
Properties: analyticstypes.NewPropertiesFromMap(properties),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: group,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) IdentifyGroup(ctx context.Context, group string, traits map[string]any) {
|
||||
if traits == nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping identify", "group", group)
|
||||
return
|
||||
}
|
||||
|
||||
// identify the user
|
||||
err := provider.client.Enqueue(analyticstypes.Identify{
|
||||
UserId: "stats_" + group,
|
||||
Traits: analyticstypes.NewTraitsFromMap(traits),
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
|
||||
// identify the group using the stats user
|
||||
err = provider.client.Enqueue(analyticstypes.Group{
|
||||
UserId: "stats_" + group,
|
||||
GroupId: group,
|
||||
Traits: analyticstypes.NewTraitsFromMap(traits),
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) IdentifyUser(ctx context.Context, group, user string, traits map[string]any) {
|
||||
if traits == nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "empty attributes received, skipping identify", "user", user, "group", group)
|
||||
return
|
||||
}
|
||||
|
||||
// identify the user
|
||||
err := provider.client.Enqueue(analyticstypes.Identify{
|
||||
UserId: user,
|
||||
Traits: analyticstypes.NewTraitsFromMap(traits),
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
|
||||
// associate the user with the group
|
||||
err = provider.client.Enqueue(analyticstypes.Group{
|
||||
UserId: user,
|
||||
GroupId: group,
|
||||
Traits: analyticstypes.NewTraits().Set("id", group), // A trait is required
|
||||
})
|
||||
if err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to send message to segment", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
if err := provider.client.Close(); err != nil {
|
||||
provider.settings.Logger().WarnContext(ctx, "unable to close segment client", "err", err)
|
||||
|
||||
@@ -70,7 +70,7 @@ func parseFieldKeyRequest(r *http.Request) (*telemetrytypes.FieldKeySelector, er
|
||||
}
|
||||
}
|
||||
|
||||
name := r.URL.Query().Get("searchText")
|
||||
name := r.URL.Query().Get("name")
|
||||
|
||||
req = telemetrytypes.FieldKeySelector{
|
||||
StartUnixMilli: startUnixMilli,
|
||||
@@ -92,10 +92,8 @@ func parseFieldValueRequest(r *http.Request) (*telemetrytypes.FieldValueSelector
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to parse field key request")
|
||||
}
|
||||
|
||||
name := r.URL.Query().Get("name")
|
||||
keySelector.Name = name
|
||||
existingQuery := r.URL.Query().Get("existingQuery")
|
||||
value := r.URL.Query().Get("searchText")
|
||||
value := r.URL.Query().Get("value")
|
||||
|
||||
// Parse limit for fieldValue request, fallback to default 50 if parsing fails.
|
||||
limit, err := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -45,19 +44,7 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
module.analytics.Send(ctx,
|
||||
analyticstypes.Track{
|
||||
UserId: creator.String(),
|
||||
Event: "Dashboard Created",
|
||||
Properties: analyticstypes.NewPropertiesFromMap(dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard})),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: orgID,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
module.analytics.TrackUser(ctx, orgID.String(), creator.String(), "Dashboard Created", dashboardtypes.NewStatsFromStorableDashboards([]*dashboardtypes.StorableDashboard{storableDashboard}))
|
||||
return dashboard, nil
|
||||
}
|
||||
|
||||
|
||||
792
pkg/modules/tracefunnel/clickhouse_queries.go
Normal file
792
pkg/modules/tracefunnel/clickhouse_queries.go
Normal file
@@ -0,0 +1,792 @@
|
||||
package tracefunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func BuildTwoStepFunnelValidationQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
) string {
|
||||
queryTemplate := `
|
||||
WITH
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2,
|
||||
toDateTime64(%[3]d/1e9, 9) AS start_ts,
|
||||
toDateTime64(%[4]d/1e9, 9) AS end_ts,
|
||||
|
||||
('%[5]s','%[6]s') AS step1,
|
||||
('%[7]s','%[8]s') AS step2
|
||||
|
||||
SELECT
|
||||
trace_id
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[9]s)
|
||||
OR
|
||||
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[10]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0
|
||||
)
|
||||
ORDER BY t1_time
|
||||
LIMIT 5;`
|
||||
return fmt.Sprintf(queryTemplate,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildThreeStepFunnelValidationQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
containsErrorT3 int,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
serviceNameT3 string,
|
||||
spanNameT3 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
clauseStep3 string,
|
||||
) string {
|
||||
queryTemplate := `
|
||||
WITH
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2,
|
||||
%[3]d AS contains_error_t3,
|
||||
toDateTime64(%[4]d/1e9, 9) AS start_ts,
|
||||
toDateTime64(%[5]d/1e9, 9) AS end_ts,
|
||||
|
||||
('%[6]s','%[7]s') AS step1,
|
||||
('%[8]s','%[9]s') AS step2,
|
||||
('%[10]s','%[11]s') AS step3
|
||||
|
||||
SELECT
|
||||
trace_id
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
|
||||
minIf(timestamp, serviceName = step3.1 AND name = step3.2) AS t3_time
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[12]s)
|
||||
OR (serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[13]s)
|
||||
OR (serviceName = step3.1 AND name = step3.2 AND (contains_error_t3 = 0 OR has_error = true) %[14]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0
|
||||
)
|
||||
ORDER BY t1_time
|
||||
LIMIT 5;`
|
||||
return fmt.Sprintf(queryTemplate,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
containsErrorT3,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
serviceNameT3,
|
||||
spanNameT3,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
clauseStep3,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildTwoStepFunnelOverviewQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
latencyPointerT1 string,
|
||||
latencyPointerT2 string,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
) string {
|
||||
queryTemplate := `
|
||||
WITH
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2,
|
||||
'%[3]s' AS latency_pointer_t1,
|
||||
'%[4]s' AS latency_pointer_t2,
|
||||
toDateTime64(%[5]d/1e9, 9) AS start_ts,
|
||||
toDateTime64(%[6]d/1e9, 9) AS end_ts,
|
||||
(%[6]d - %[5]d)/1e9 AS time_window_sec,
|
||||
|
||||
('%[7]s','%[8]s') AS step1,
|
||||
('%[9]s','%[10]s') AS step2
|
||||
|
||||
, funnel AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
|
||||
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS s1_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS s2_error
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[11]s)
|
||||
OR
|
||||
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[12]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0
|
||||
)
|
||||
|
||||
, totals AS (
|
||||
SELECT
|
||||
count(DISTINCT trace_id) AS total_s1_spans,
|
||||
count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
|
||||
count(DISTINCT CASE WHEN s1_error = 1 THEN trace_id END) AS sum_s1_error,
|
||||
count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) AS sum_s2_error,
|
||||
avgIf((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time))/1e6, t1_time > 0 AND t2_time > t1_time) AS avg_duration,
|
||||
quantileIf(0.99)((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time))/1e6, t1_time > 0 AND t2_time > t1_time) AS latency
|
||||
FROM funnel
|
||||
)
|
||||
|
||||
SELECT
|
||||
round(if(total_s1_spans > 0, total_s2_spans * 100.0 / total_s1_spans, 0), 2) AS conversion_rate,
|
||||
total_s2_spans / time_window_sec AS avg_rate,
|
||||
greatest(sum_s1_error, sum_s2_error) AS errors,
|
||||
avg_duration,
|
||||
latency
|
||||
FROM totals;
|
||||
`
|
||||
return fmt.Sprintf(queryTemplate,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
latencyPointerT1,
|
||||
latencyPointerT2,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildThreeStepFunnelOverviewQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
containsErrorT3 int,
|
||||
latencyPointerT1 string,
|
||||
latencyPointerT2 string,
|
||||
latencyPointerT3 string,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
serviceNameT3 string,
|
||||
spanNameT3 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
clauseStep3 string,
|
||||
) string {
|
||||
queryTemplate := `
|
||||
WITH
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2,
|
||||
%[3]d AS contains_error_t3,
|
||||
'%[4]s' AS latency_pointer_t1,
|
||||
'%[5]s' AS latency_pointer_t2,
|
||||
'%[6]s' AS latency_pointer_t3,
|
||||
toDateTime64(%[7]d/1e9, 9) AS start_ts,
|
||||
toDateTime64(%[8]d/1e9, 9) AS end_ts,
|
||||
(%[8]d - %[7]d)/1e9 AS time_window_sec,
|
||||
|
||||
('%[9]s','%[10]s') AS step1,
|
||||
('%[11]s','%[12]s') AS step2,
|
||||
('%[13]s','%[14]s') AS step3
|
||||
|
||||
, funnel AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
|
||||
minIf(timestamp, serviceName = step3.1 AND name = step3.2) AS t3_time,
|
||||
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS s1_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS s2_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step3.1 AND name = step3.2)) AS s3_error
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[15]s)
|
||||
OR (serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[16]s)
|
||||
OR (serviceName = step3.1 AND name = step3.2 AND (contains_error_t3 = 0 OR has_error = true) %[17]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0
|
||||
)
|
||||
|
||||
, totals AS (
|
||||
SELECT
|
||||
count(DISTINCT trace_id) AS total_s1_spans,
|
||||
count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
|
||||
count(DISTINCT CASE WHEN t3_time > t2_time AND t2_time > t1_time THEN trace_id END) AS total_s3_spans,
|
||||
|
||||
count(DISTINCT CASE WHEN s1_error = 1 THEN trace_id END) AS sum_s1_error,
|
||||
count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) AS sum_s2_error,
|
||||
count(DISTINCT CASE WHEN s3_error = 1 THEN trace_id END) AS sum_s3_error,
|
||||
|
||||
avgIf((toUnixTimestamp64Nano(t3_time) - toUnixTimestamp64Nano(t1_time))/1e6, t1_time > 0 AND t2_time > t1_time AND t3_time > t2_time) AS avg_funnel_duration,
|
||||
quantileIf(0.99)((toUnixTimestamp64Nano(t3_time) - toUnixTimestamp64Nano(t1_time))/1e6, t1_time > 0 AND t2_time > t1_time AND t3_time > t2_time) AS p99_funnel_latency
|
||||
|
||||
|
||||
FROM funnel
|
||||
)
|
||||
|
||||
SELECT
|
||||
round(if(total_s1_spans > 0, total_s3_spans * 100.0 / total_s1_spans, 0), 2) AS conversion_rate,
|
||||
total_s3_spans / nullIf(time_window_sec, 0) AS avg_rate,
|
||||
greatest(sum_s1_error, sum_s2_error, sum_s3_error) AS errors,
|
||||
avg_funnel_duration AS avg_duration,
|
||||
p99_funnel_latency AS latency
|
||||
FROM totals;
|
||||
`
|
||||
return fmt.Sprintf(
|
||||
queryTemplate,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
containsErrorT3,
|
||||
latencyPointerT1,
|
||||
latencyPointerT2,
|
||||
latencyPointerT3,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
serviceNameT3,
|
||||
spanNameT3,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
clauseStep3,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildTwoStepFunnelCountQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
) string {
|
||||
queryTemplate := `
|
||||
WITH
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2,
|
||||
toDateTime64(%[3]d/1e9,9) AS start_ts,
|
||||
toDateTime64(%[4]d/1e9,9) AS end_ts,
|
||||
|
||||
('%[5]s','%[6]s') AS step1,
|
||||
('%[7]s','%[8]s') AS step2
|
||||
|
||||
SELECT
|
||||
count(DISTINCT trace_id) AS total_s1_spans,
|
||||
count(DISTINCT CASE WHEN t1_error = 1 THEN trace_id END) AS total_s1_errored_spans,
|
||||
count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
|
||||
count(DISTINCT CASE WHEN t2_time > t1_time AND t2_error = 1 THEN trace_id END) AS total_s2_errored_spans
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
|
||||
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS t1_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS t2_error
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[9]s)
|
||||
OR
|
||||
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[10]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0
|
||||
) AS funnel;
|
||||
`
|
||||
return fmt.Sprintf(queryTemplate,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildThreeStepFunnelCountQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
containsErrorT3 int,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
serviceNameT3 string,
|
||||
spanNameT3 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
clauseStep3 string,
|
||||
) string {
|
||||
queryTemplate := `
|
||||
WITH
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2,
|
||||
%[3]d AS contains_error_t3,
|
||||
toDateTime64(%[4]d/1e9,9) AS start_ts,
|
||||
toDateTime64(%[5]d/1e9,9) AS end_ts,
|
||||
|
||||
('%[6]s','%[7]s') AS step1,
|
||||
('%[8]s','%[9]s') AS step2,
|
||||
('%[10]s','%[11]s') AS step3
|
||||
|
||||
SELECT
|
||||
count(DISTINCT trace_id) AS total_s1_spans,
|
||||
count(DISTINCT CASE WHEN t1_error = 1 THEN trace_id END) AS total_s1_errored_spans,
|
||||
count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
|
||||
count(DISTINCT CASE WHEN t2_time > t1_time AND t2_error = 1 THEN trace_id END) AS total_s2_errored_spans,
|
||||
count(DISTINCT CASE WHEN t3_time > t2_time AND t2_time > t1_time THEN trace_id END) AS total_s3_spans,
|
||||
count(DISTINCT CASE WHEN t3_time > t2_time AND t2_time > t1_time AND t3_error = 1 THEN trace_id END) AS total_s3_errored_spans
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
|
||||
minIf(timestamp, serviceName = step3.1 AND name = step3.2) AS t3_time,
|
||||
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS t1_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS t2_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step3.1 AND name = step3.2)) AS t3_error
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[12]s)
|
||||
OR (serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[13]s)
|
||||
OR (serviceName = step3.1 AND name = step3.2 AND (contains_error_t3 = 0 OR has_error = true) %[14]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0
|
||||
) AS funnel;
|
||||
`
|
||||
return fmt.Sprintf(queryTemplate,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
containsErrorT3,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
serviceNameT3,
|
||||
spanNameT3,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
clauseStep3,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildTwoStepFunnelTopSlowTracesQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
) string {
|
||||
queryTemplate := `
|
||||
WITH
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2,
|
||||
toDateTime64(%[3]d/1e9, 9) AS start_ts,
|
||||
toDateTime64(%[4]d/1e9, 9) AS end_ts,
|
||||
|
||||
('%[5]s','%[6]s') AS step1,
|
||||
('%[7]s','%[8]s') AS step2
|
||||
|
||||
SELECT
|
||||
trace_id,
|
||||
(toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6 AS duration_ms,
|
||||
span_count
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
|
||||
count() AS span_count
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[9]s)
|
||||
OR
|
||||
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[10]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0 AND t2_time > t1_time
|
||||
) AS funnel
|
||||
ORDER BY duration_ms DESC
|
||||
LIMIT 5;
|
||||
`
|
||||
return fmt.Sprintf(queryTemplate,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildTwoStepFunnelTopSlowErrorTracesQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
) string {
|
||||
queryTemplate := `
|
||||
WITH
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2,
|
||||
toDateTime64(%[3]d/1e9, 9) AS start_ts,
|
||||
toDateTime64(%[4]d/1e9, 9) AS end_ts,
|
||||
|
||||
('%[5]s','%[6]s') AS step1,
|
||||
('%[7]s','%[8]s') AS step2
|
||||
|
||||
SELECT
|
||||
trace_id,
|
||||
(toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6 AS duration_ms,
|
||||
span_count
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
|
||||
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS t1_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS t2_error,
|
||||
count() AS span_count
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[9]s)
|
||||
OR
|
||||
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[10]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0 AND t2_time > t1_time
|
||||
) AS funnel
|
||||
WHERE
|
||||
(t1_error = 1 OR t2_error = 1)
|
||||
ORDER BY duration_ms DESC
|
||||
LIMIT 5;
|
||||
`
|
||||
return fmt.Sprintf(queryTemplate,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildTwoStepFunnelStepOverviewQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
latencyPointerT1 string,
|
||||
latencyPointerT2 string,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
latencyTypeT2 string,
|
||||
) string {
|
||||
const tpl = `
|
||||
WITH
|
||||
toDateTime64(%[5]d / 1e9, 9) AS start_ts,
|
||||
toDateTime64(%[6]d / 1e9, 9) AS end_ts,
|
||||
(%[6]d - %[5]d) / 1e9 AS time_window_sec,
|
||||
|
||||
('%[7]s', '%[8]s') AS step1,
|
||||
('%[9]s', '%[10]s') AS step2,
|
||||
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2
|
||||
|
||||
SELECT
|
||||
round(total_s2_spans * 100.0 / total_s1_spans, 2) AS conversion_rate,
|
||||
total_s2_spans / time_window_sec AS avg_rate,
|
||||
greatest(sum_s1_error, sum_s2_error) AS errors,
|
||||
avg_duration,
|
||||
latency
|
||||
FROM (
|
||||
SELECT
|
||||
count(DISTINCT trace_id) AS total_s1_spans,
|
||||
count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
|
||||
count(DISTINCT CASE WHEN s1_error = 1 THEN trace_id END) AS sum_s1_error,
|
||||
count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) AS sum_s2_error,
|
||||
|
||||
avgIf(
|
||||
(toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6,
|
||||
t1_time > 0 AND t2_time > t1_time
|
||||
) AS avg_duration,
|
||||
|
||||
quantileIf(%[13]s)(
|
||||
(toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6,
|
||||
t1_time > 0 AND t2_time > t1_time
|
||||
) AS latency
|
||||
FROM (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
|
||||
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS s1_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS s2_error
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[11]s)
|
||||
OR
|
||||
(serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[12]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0
|
||||
) AS funnel
|
||||
) AS totals;
|
||||
`
|
||||
|
||||
return fmt.Sprintf(tpl,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
latencyPointerT1,
|
||||
latencyPointerT2,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
latencyTypeT2,
|
||||
)
|
||||
}
|
||||
|
||||
func BuildThreeStepFunnelStepOverviewQuery(
|
||||
containsErrorT1 int,
|
||||
containsErrorT2 int,
|
||||
containsErrorT3 int,
|
||||
latencyPointerT1 string,
|
||||
latencyPointerT2 string,
|
||||
latencyPointerT3 string,
|
||||
startTs int64,
|
||||
endTs int64,
|
||||
serviceNameT1 string,
|
||||
spanNameT1 string,
|
||||
serviceNameT2 string,
|
||||
spanNameT2 string,
|
||||
serviceNameT3 string,
|
||||
spanNameT3 string,
|
||||
clauseStep1 string,
|
||||
clauseStep2 string,
|
||||
clauseStep3 string,
|
||||
stepStart int64,
|
||||
stepEnd int64,
|
||||
latencyTypeT2 string,
|
||||
latencyTypeT3 string,
|
||||
) string {
|
||||
const baseWithAndFunnel = `
|
||||
WITH
|
||||
toDateTime64(%[7]d/1e9, 9) AS start_ts,
|
||||
toDateTime64(%[8]d/1e9, 9) AS end_ts,
|
||||
(%[8]d - %[7]d) / 1e9 AS time_window_sec,
|
||||
|
||||
('%[9]s','%[10]s') AS step1,
|
||||
('%[11]s','%[12]s') AS step2,
|
||||
('%[13]s','%[14]s') AS step3,
|
||||
|
||||
%[1]d AS contains_error_t1,
|
||||
%[2]d AS contains_error_t2,
|
||||
%[3]d AS contains_error_t3,
|
||||
|
||||
funnel AS (
|
||||
SELECT
|
||||
trace_id,
|
||||
minIf(timestamp, serviceName = step1.1 AND name = step1.2) AS t1_time,
|
||||
minIf(timestamp, serviceName = step2.1 AND name = step2.2) AS t2_time,
|
||||
minIf(timestamp, serviceName = step3.1 AND name = step3.2) AS t3_time,
|
||||
toUInt8(anyIf(has_error, serviceName = step1.1 AND name = step1.2)) AS s1_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step2.1 AND name = step2.2)) AS s2_error,
|
||||
toUInt8(anyIf(has_error, serviceName = step3.1 AND name = step3.2)) AS s3_error
|
||||
FROM signoz_traces.distributed_signoz_index_v3
|
||||
WHERE
|
||||
timestamp BETWEEN start_ts AND end_ts
|
||||
AND (
|
||||
(serviceName = step1.1 AND name = step1.2 AND (contains_error_t1 = 0 OR has_error = true) %[15]s)
|
||||
OR (serviceName = step2.1 AND name = step2.2 AND (contains_error_t2 = 0 OR has_error = true) %[16]s)
|
||||
OR (serviceName = step3.1 AND name = step3.2 AND (contains_error_t3 = 0 OR has_error = true) %[17]s)
|
||||
)
|
||||
GROUP BY trace_id
|
||||
HAVING t1_time > 0
|
||||
)
|
||||
`
|
||||
|
||||
const totals12 = `
|
||||
SELECT
|
||||
round(if(total_s1_spans > 0, total_s2_spans * 100.0 / total_s1_spans, 0), 2) AS conversion_rate,
|
||||
total_s2_spans / time_window_sec AS avg_rate,
|
||||
greatest(sum_s1_error, sum_s2_error) AS errors,
|
||||
avg_duration_12 AS avg_duration,
|
||||
latency_12 AS latency
|
||||
FROM (
|
||||
SELECT
|
||||
count(DISTINCT CASE WHEN t2_time > t1_time THEN trace_id END) AS total_s2_spans,
|
||||
count(DISTINCT trace_id) AS total_s1_spans,
|
||||
count(DISTINCT CASE WHEN s1_error = 1 THEN trace_id END) AS sum_s1_error,
|
||||
count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) AS sum_s2_error,
|
||||
avgIf((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6, t1_time > 0 AND t2_time > t1_time) AS avg_duration_12,
|
||||
quantileIf(%[18]s)((toUnixTimestamp64Nano(t2_time) - toUnixTimestamp64Nano(t1_time)) / 1e6, t1_time > 0 AND t2_time > t1_time) AS latency_12
|
||||
FROM funnel
|
||||
) AS totals;
|
||||
`
|
||||
|
||||
const totals23 = `
|
||||
SELECT
|
||||
round(if(total_s2_spans > 0, total_s3_spans * 100.0 / total_s2_spans, 0), 2) AS conversion_rate,
|
||||
total_s3_spans / time_window_sec AS avg_rate,
|
||||
greatest(sum_s2_error, sum_s3_error) AS errors,
|
||||
avg_duration_23 AS avg_duration,
|
||||
latency_23 AS latency
|
||||
FROM (
|
||||
SELECT
|
||||
count(DISTINCT CASE WHEN t2_time > 0 AND t3_time > t2_time THEN trace_id END) AS total_s3_spans,
|
||||
count(DISTINCT CASE WHEN t2_time > 0 THEN trace_id END) AS total_s2_spans,
|
||||
count(DISTINCT CASE WHEN s2_error = 1 THEN trace_id END) AS sum_s2_error,
|
||||
count(DISTINCT CASE WHEN s3_error = 1 THEN trace_id END) AS sum_s3_error,
|
||||
avgIf((toUnixTimestamp64Nano(t3_time) - toUnixTimestamp64Nano(t2_time)) / 1e6, t2_time > 0 AND t3_time > t2_time) AS avg_duration_23,
|
||||
quantileIf(%[19]s)((toUnixTimestamp64Nano(t3_time) - toUnixTimestamp64Nano(t2_time)) / 1e6, t2_time > 0 AND t3_time > t2_time) AS latency_23
|
||||
FROM funnel
|
||||
) AS totals;
|
||||
`
|
||||
|
||||
const fallback = `
|
||||
SELECT 0 AS conversion_rate, 0 AS avg_rate, 0 AS errors, 0 AS avg_duration, 0 AS latency;
|
||||
`
|
||||
|
||||
var totalsTpl string
|
||||
switch {
|
||||
case stepStart == 1 && stepEnd == 2:
|
||||
totalsTpl = totals12
|
||||
case stepStart == 2 && stepEnd == 3:
|
||||
totalsTpl = totals23
|
||||
default:
|
||||
totalsTpl = fallback
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
baseWithAndFunnel+totalsTpl,
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
containsErrorT3,
|
||||
latencyPointerT1,
|
||||
latencyPointerT2,
|
||||
latencyPointerT3,
|
||||
startTs,
|
||||
endTs,
|
||||
serviceNameT1,
|
||||
spanNameT1,
|
||||
serviceNameT2,
|
||||
spanNameT2,
|
||||
serviceNameT3,
|
||||
spanNameT3,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
clauseStep3,
|
||||
latencyTypeT2,
|
||||
latencyTypeT3,
|
||||
)
|
||||
}
|
||||
475
pkg/modules/tracefunnel/query.go
Normal file
475
pkg/modules/tracefunnel/query.go
Normal file
@@ -0,0 +1,475 @@
|
||||
package tracefunnel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
tracev4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4"
|
||||
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
|
||||
"github.com/SigNoz/signoz/pkg/types/tracefunneltypes"
|
||||
)
|
||||
|
||||
// sanitizeClause adds AND prefix to non-empty clauses if not already present
|
||||
func sanitizeClause(clause string) string {
|
||||
if clause == "" {
|
||||
return ""
|
||||
}
|
||||
// Check if clause already starts with AND
|
||||
if strings.HasPrefix(strings.TrimSpace(clause), "AND") {
|
||||
return clause
|
||||
}
|
||||
return "AND " + clause
|
||||
}
|
||||
|
||||
func ValidateTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
||||
var query string
|
||||
var err error
|
||||
|
||||
funnelSteps := funnel.Steps
|
||||
containsErrorT1 := 0
|
||||
containsErrorT2 := 0
|
||||
containsErrorT3 := 0
|
||||
|
||||
if funnelSteps[0].HasErrors {
|
||||
containsErrorT1 = 1
|
||||
}
|
||||
if funnelSteps[1].HasErrors {
|
||||
containsErrorT2 = 1
|
||||
}
|
||||
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
|
||||
containsErrorT3 = 1
|
||||
}
|
||||
|
||||
// Build filter clauses for each step
|
||||
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[0].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[1].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep3 := ""
|
||||
if len(funnel.Steps) > 2 {
|
||||
clauseStep3, err = tracev4.BuildTracesFilter(funnelSteps[2].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize clauses
|
||||
clauseStep1 = sanitizeClause(clauseStep1)
|
||||
clauseStep2 = sanitizeClause(clauseStep2)
|
||||
clauseStep3 = sanitizeClause(clauseStep3)
|
||||
|
||||
if len(funnel.Steps) > 2 {
|
||||
query = BuildThreeStepFunnelValidationQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
containsErrorT3,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[0].ServiceName,
|
||||
funnelSteps[0].SpanName,
|
||||
funnelSteps[1].ServiceName,
|
||||
funnelSteps[1].SpanName,
|
||||
funnelSteps[2].ServiceName,
|
||||
funnelSteps[2].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
clauseStep3,
|
||||
)
|
||||
} else {
|
||||
query = BuildTwoStepFunnelValidationQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[0].ServiceName,
|
||||
funnelSteps[0].SpanName,
|
||||
funnelSteps[1].ServiceName,
|
||||
funnelSteps[1].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
}
|
||||
|
||||
return &v3.ClickHouseQuery{
|
||||
Query: query,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetFunnelAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
||||
var query string
|
||||
var err error
|
||||
|
||||
funnelSteps := funnel.Steps
|
||||
containsErrorT1 := 0
|
||||
containsErrorT2 := 0
|
||||
containsErrorT3 := 0
|
||||
latencyPointerT1 := funnelSteps[0].LatencyPointer
|
||||
latencyPointerT2 := funnelSteps[1].LatencyPointer
|
||||
latencyPointerT3 := "start"
|
||||
if len(funnel.Steps) > 2 {
|
||||
latencyPointerT3 = funnelSteps[2].LatencyPointer
|
||||
}
|
||||
|
||||
if funnelSteps[0].HasErrors {
|
||||
containsErrorT1 = 1
|
||||
}
|
||||
if funnelSteps[1].HasErrors {
|
||||
containsErrorT2 = 1
|
||||
}
|
||||
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
|
||||
containsErrorT3 = 1
|
||||
}
|
||||
|
||||
// Build filter clauses for each step
|
||||
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[0].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[1].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep3 := ""
|
||||
if len(funnel.Steps) > 2 {
|
||||
clauseStep3, err = tracev4.BuildTracesFilter(funnelSteps[2].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize clauses
|
||||
clauseStep1 = sanitizeClause(clauseStep1)
|
||||
clauseStep2 = sanitizeClause(clauseStep2)
|
||||
clauseStep3 = sanitizeClause(clauseStep3)
|
||||
|
||||
if len(funnel.Steps) > 2 {
|
||||
query = BuildThreeStepFunnelOverviewQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
containsErrorT3,
|
||||
latencyPointerT1,
|
||||
latencyPointerT2,
|
||||
latencyPointerT3,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[0].ServiceName,
|
||||
funnelSteps[0].SpanName,
|
||||
funnelSteps[1].ServiceName,
|
||||
funnelSteps[1].SpanName,
|
||||
funnelSteps[2].ServiceName,
|
||||
funnelSteps[2].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
clauseStep3,
|
||||
)
|
||||
} else {
|
||||
query = BuildTwoStepFunnelOverviewQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
latencyPointerT1,
|
||||
latencyPointerT2,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[0].ServiceName,
|
||||
funnelSteps[0].SpanName,
|
||||
funnelSteps[1].ServiceName,
|
||||
funnelSteps[1].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
}
|
||||
return &v3.ClickHouseQuery{Query: query}, nil
|
||||
}
|
||||
|
||||
func GetFunnelStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
|
||||
var query string
|
||||
var err error
|
||||
|
||||
funnelSteps := funnel.Steps
|
||||
containsErrorT1 := 0
|
||||
containsErrorT2 := 0
|
||||
containsErrorT3 := 0
|
||||
latencyPointerT1 := funnelSteps[0].LatencyPointer
|
||||
latencyPointerT2 := funnelSteps[1].LatencyPointer
|
||||
latencyPointerT3 := "start"
|
||||
if len(funnel.Steps) > 2 {
|
||||
latencyPointerT3 = funnelSteps[2].LatencyPointer
|
||||
}
|
||||
latencyTypeT2 := "0.99"
|
||||
latencyTypeT3 := "0.99"
|
||||
|
||||
if stepStart == stepEnd {
|
||||
return nil, fmt.Errorf("step start and end cannot be the same for /step/overview")
|
||||
}
|
||||
|
||||
if funnelSteps[0].HasErrors {
|
||||
containsErrorT1 = 1
|
||||
}
|
||||
if funnelSteps[1].HasErrors {
|
||||
containsErrorT2 = 1
|
||||
}
|
||||
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
|
||||
containsErrorT3 = 1
|
||||
}
|
||||
|
||||
if funnelSteps[1].LatencyType != "" {
|
||||
latency := strings.ToLower(funnelSteps[1].LatencyType)
|
||||
if latency == "p90" {
|
||||
latencyTypeT2 = "0.90"
|
||||
} else if latency == "p95" {
|
||||
latencyTypeT2 = "0.95"
|
||||
} else {
|
||||
latencyTypeT2 = "0.99"
|
||||
}
|
||||
}
|
||||
if len(funnel.Steps) > 2 && funnelSteps[2].LatencyType != "" {
|
||||
latency := strings.ToLower(funnelSteps[2].LatencyType)
|
||||
if latency == "p90" {
|
||||
latencyTypeT3 = "0.90"
|
||||
} else if latency == "p95" {
|
||||
latencyTypeT3 = "0.95"
|
||||
} else {
|
||||
latencyTypeT3 = "0.99"
|
||||
}
|
||||
}
|
||||
|
||||
// Build filter clauses for each step
|
||||
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[0].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[1].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep3 := ""
|
||||
if len(funnel.Steps) > 2 {
|
||||
clauseStep3, err = tracev4.BuildTracesFilter(funnelSteps[2].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize clauses
|
||||
clauseStep1 = sanitizeClause(clauseStep1)
|
||||
clauseStep2 = sanitizeClause(clauseStep2)
|
||||
clauseStep3 = sanitizeClause(clauseStep3)
|
||||
|
||||
if len(funnel.Steps) > 2 {
|
||||
query = BuildThreeStepFunnelStepOverviewQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
containsErrorT3,
|
||||
latencyPointerT1,
|
||||
latencyPointerT2,
|
||||
latencyPointerT3,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[0].ServiceName,
|
||||
funnelSteps[0].SpanName,
|
||||
funnelSteps[1].ServiceName,
|
||||
funnelSteps[1].SpanName,
|
||||
funnelSteps[2].ServiceName,
|
||||
funnelSteps[2].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
clauseStep3,
|
||||
stepStart,
|
||||
stepEnd,
|
||||
latencyTypeT2,
|
||||
latencyTypeT3,
|
||||
)
|
||||
} else {
|
||||
query = BuildTwoStepFunnelStepOverviewQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
latencyPointerT1,
|
||||
latencyPointerT2,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[0].ServiceName,
|
||||
funnelSteps[0].SpanName,
|
||||
funnelSteps[1].ServiceName,
|
||||
funnelSteps[1].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
latencyTypeT2,
|
||||
)
|
||||
}
|
||||
return &v3.ClickHouseQuery{Query: query}, nil
|
||||
}
|
||||
|
||||
func GetStepAnalytics(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange) (*v3.ClickHouseQuery, error) {
|
||||
var query string
|
||||
|
||||
funnelSteps := funnel.Steps
|
||||
containsErrorT1 := 0
|
||||
containsErrorT2 := 0
|
||||
containsErrorT3 := 0
|
||||
|
||||
if funnelSteps[0].HasErrors {
|
||||
containsErrorT1 = 1
|
||||
}
|
||||
if funnelSteps[1].HasErrors {
|
||||
containsErrorT2 = 1
|
||||
}
|
||||
if len(funnel.Steps) > 2 && funnelSteps[2].HasErrors {
|
||||
containsErrorT3 = 1
|
||||
}
|
||||
|
||||
// Build filter clauses for each step
|
||||
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[0].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[1].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep3 := ""
|
||||
if len(funnel.Steps) > 2 {
|
||||
clauseStep3, err = tracev4.BuildTracesFilter(funnelSteps[2].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Sanitize clauses
|
||||
clauseStep1 = sanitizeClause(clauseStep1)
|
||||
clauseStep2 = sanitizeClause(clauseStep2)
|
||||
clauseStep3 = sanitizeClause(clauseStep3)
|
||||
|
||||
if len(funnel.Steps) > 2 {
|
||||
query = BuildThreeStepFunnelCountQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
containsErrorT3,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[0].ServiceName,
|
||||
funnelSteps[0].SpanName,
|
||||
funnelSteps[1].ServiceName,
|
||||
funnelSteps[1].SpanName,
|
||||
funnelSteps[2].ServiceName,
|
||||
funnelSteps[2].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
clauseStep3,
|
||||
)
|
||||
} else {
|
||||
query = BuildTwoStepFunnelCountQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[0].ServiceName,
|
||||
funnelSteps[0].SpanName,
|
||||
funnelSteps[1].ServiceName,
|
||||
funnelSteps[1].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
}
|
||||
|
||||
return &v3.ClickHouseQuery{
|
||||
Query: query,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GetSlowestTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
|
||||
funnelSteps := funnel.Steps
|
||||
containsErrorT1 := 0
|
||||
containsErrorT2 := 0
|
||||
stepStartOrder := 0
|
||||
stepEndOrder := 1
|
||||
|
||||
if stepStart != stepEnd {
|
||||
stepStartOrder = int(stepStart) - 1
|
||||
stepEndOrder = int(stepEnd) - 1
|
||||
if funnelSteps[stepStartOrder].HasErrors {
|
||||
containsErrorT1 = 1
|
||||
}
|
||||
if funnelSteps[stepEndOrder].HasErrors {
|
||||
containsErrorT2 = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Build filter clauses for the steps
|
||||
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[stepStartOrder].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[stepEndOrder].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sanitize clauses
|
||||
clauseStep1 = sanitizeClause(clauseStep1)
|
||||
clauseStep2 = sanitizeClause(clauseStep2)
|
||||
|
||||
query := BuildTwoStepFunnelTopSlowTracesQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[stepStartOrder].ServiceName,
|
||||
funnelSteps[stepStartOrder].SpanName,
|
||||
funnelSteps[stepEndOrder].ServiceName,
|
||||
funnelSteps[stepEndOrder].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
return &v3.ClickHouseQuery{Query: query}, nil
|
||||
}
|
||||
|
||||
func GetErroredTraces(funnel *tracefunneltypes.StorableFunnel, timeRange tracefunneltypes.TimeRange, stepStart, stepEnd int64) (*v3.ClickHouseQuery, error) {
|
||||
funnelSteps := funnel.Steps
|
||||
containsErrorT1 := 0
|
||||
containsErrorT2 := 0
|
||||
stepStartOrder := 0
|
||||
stepEndOrder := 1
|
||||
|
||||
if stepStart != stepEnd {
|
||||
stepStartOrder = int(stepStart) - 1
|
||||
stepEndOrder = int(stepEnd) - 1
|
||||
if funnelSteps[stepStartOrder].HasErrors {
|
||||
containsErrorT1 = 1
|
||||
}
|
||||
if funnelSteps[stepEndOrder].HasErrors {
|
||||
containsErrorT2 = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Build filter clauses for the steps
|
||||
clauseStep1, err := tracev4.BuildTracesFilter(funnelSteps[stepStartOrder].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clauseStep2, err := tracev4.BuildTracesFilter(funnelSteps[stepEndOrder].Filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sanitize clauses
|
||||
clauseStep1 = sanitizeClause(clauseStep1)
|
||||
clauseStep2 = sanitizeClause(clauseStep2)
|
||||
|
||||
query := BuildTwoStepFunnelTopSlowErrorTracesQuery(
|
||||
containsErrorT1,
|
||||
containsErrorT2,
|
||||
timeRange.StartTime,
|
||||
timeRange.EndTime,
|
||||
funnelSteps[stepStartOrder].ServiceName,
|
||||
funnelSteps[stepStartOrder].SpanName,
|
||||
funnelSteps[stepEndOrder].ServiceName,
|
||||
funnelSteps[stepEndOrder].SpanName,
|
||||
clauseStep1,
|
||||
clauseStep2,
|
||||
)
|
||||
return &v3.ClickHouseQuery{Query: query}, nil
|
||||
}
|
||||
31
pkg/modules/user/impluser/getter.go
Normal file
31
pkg/modules/user/impluser/getter.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package impluser
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type getter struct {
|
||||
store types.UserStore
|
||||
}
|
||||
|
||||
func NewGetter(store types.UserStore) user.Getter {
|
||||
return &getter{store: store}
|
||||
}
|
||||
|
||||
func (module *getter) ListByOrgID(ctx context.Context, orgID valuer.UUID) ([]*types.User, error) {
|
||||
gettableUsers, err := module.store.ListUsers(ctx, orgID.StringValue())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*types.User, len(gettableUsers))
|
||||
for i, user := range gettableUsers {
|
||||
users[i] = &user.User
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
@@ -326,7 +326,7 @@ func (h *handler) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
updatedUser, err := h.module.UpdateUser(ctx, claims.OrgID, id, &user)
|
||||
updatedUser, err := h.module.UpdateUser(ctx, claims.OrgID, id, &user, claims.UserID)
|
||||
if err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
@@ -347,7 +347,7 @@ func (h *handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.DeleteUser(ctx, claims.OrgID, id); err != nil {
|
||||
if err := h.module.DeleteUser(ctx, claims.OrgID, id, claims.UserID); err != nil {
|
||||
render.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/query-service/model"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/telemetry"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/emailtypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -135,35 +134,9 @@ func (m *Module) CreateUserWithPassword(ctx context.Context, user *types.User, p
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.analytics.Send(ctx,
|
||||
analyticstypes.Identify{
|
||||
UserId: user.ID.String(),
|
||||
Traits: analyticstypes.
|
||||
NewTraits().
|
||||
SetName(user.DisplayName).
|
||||
SetEmail(user.Email).
|
||||
Set("role", user.Role).
|
||||
SetCreatedAt(user.CreatedAt),
|
||||
},
|
||||
analyticstypes.Group{
|
||||
UserId: user.ID.String(),
|
||||
GroupId: user.OrgID,
|
||||
},
|
||||
analyticstypes.Track{
|
||||
UserId: user.ID.String(),
|
||||
Event: "User Created",
|
||||
Properties: analyticstypes.NewPropertiesFromMap(map[string]any{
|
||||
"role": user.Role,
|
||||
"email": user.Email,
|
||||
"name": user.DisplayName,
|
||||
}),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: user.OrgID,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
traitsOrProperties := types.NewTraitsFromUser(user)
|
||||
m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traitsOrProperties)
|
||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Created", traitsOrProperties)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
@@ -173,35 +146,9 @@ func (m *Module) CreateUser(ctx context.Context, user *types.User) error {
|
||||
return err
|
||||
}
|
||||
|
||||
m.analytics.Send(ctx,
|
||||
analyticstypes.Identify{
|
||||
UserId: user.ID.String(),
|
||||
Traits: analyticstypes.
|
||||
NewTraits().
|
||||
SetName(user.DisplayName).
|
||||
SetEmail(user.Email).
|
||||
Set("role", user.Role).
|
||||
SetCreatedAt(user.CreatedAt),
|
||||
},
|
||||
analyticstypes.Group{
|
||||
UserId: user.ID.String(),
|
||||
GroupId: user.OrgID,
|
||||
},
|
||||
analyticstypes.Track{
|
||||
UserId: user.ID.String(),
|
||||
Event: "User Created",
|
||||
Properties: analyticstypes.NewPropertiesFromMap(map[string]any{
|
||||
"role": user.Role,
|
||||
"email": user.Email,
|
||||
"name": user.DisplayName,
|
||||
}),
|
||||
Context: &analyticstypes.Context{
|
||||
Extra: map[string]interface{}{
|
||||
analyticstypes.KeyGroupID: user.OrgID,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
traitsOrProperties := types.NewTraitsFromUser(user)
|
||||
m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traitsOrProperties)
|
||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Created", traitsOrProperties)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -226,11 +173,22 @@ func (m *Module) ListUsers(ctx context.Context, orgID string) ([]*types.Gettable
|
||||
return m.store.ListUsers(ctx, orgID)
|
||||
}
|
||||
|
||||
func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user *types.User) (*types.User, error) {
|
||||
return m.store.UpdateUser(ctx, orgID, id, user)
|
||||
func (m *Module) UpdateUser(ctx context.Context, orgID string, id string, user *types.User, updatedBy string) (*types.User, error) {
|
||||
user, err := m.store.UpdateUser(ctx, orgID, id, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
traits := types.NewTraitsFromUser(user)
|
||||
m.analytics.IdentifyUser(ctx, user.OrgID, user.ID.String(), traits)
|
||||
|
||||
traits["updated_by"] = updatedBy
|
||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Updated", traits)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (m *Module) DeleteUser(ctx context.Context, orgID string, id string) error {
|
||||
func (m *Module) DeleteUser(ctx context.Context, orgID string, id string, deletedBy string) error {
|
||||
user, err := m.store.GetUserByID(ctx, orgID, id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -250,7 +208,15 @@ func (m *Module) DeleteUser(ctx context.Context, orgID string, id string) error
|
||||
return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin")
|
||||
}
|
||||
|
||||
return m.store.DeleteUser(ctx, orgID, user.ID.StringValue())
|
||||
if err := m.store.DeleteUser(ctx, orgID, user.ID.StringValue()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.analytics.TrackUser(ctx, user.OrgID, user.ID.String(), "User Deleted", map[string]any{
|
||||
"deleted_by": deletedBy,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Module) CreateResetPasswordToken(ctx context.Context, userID string) (*types.ResetPasswordRequest, error) {
|
||||
@@ -644,10 +610,16 @@ func (m *Module) Register(ctx context.Context, req *types.PostableRegisterOrgAnd
|
||||
}
|
||||
|
||||
func (m *Module) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
|
||||
stats := make(map[string]any)
|
||||
count, err := m.store.CountByOrgID(ctx, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if err == nil {
|
||||
stats["user.count"] = count
|
||||
}
|
||||
|
||||
return map[string]any{"user.count": count}, nil
|
||||
count, err = m.store.CountAPIKeyByOrgID(ctx, orgID)
|
||||
if err == nil {
|
||||
stats["factor.api_key.count"] = count
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
@@ -826,3 +826,21 @@ func (store *store) CountByOrgID(ctx context.Context, orgID valuer.UUID) (int64,
|
||||
|
||||
return int64(count), nil
|
||||
}
|
||||
|
||||
func (store *store) CountAPIKeyByOrgID(ctx context.Context, orgID valuer.UUID) (int64, error) {
|
||||
apiKey := new(types.StorableAPIKey)
|
||||
|
||||
count, err := store.
|
||||
sqlstore.
|
||||
BunDB().
|
||||
NewSelect().
|
||||
Model(apiKey).
|
||||
Join("JOIN users ON users.id = storable_api_key.user_id").
|
||||
Where("org_id = ?", orgID).
|
||||
Count(ctx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int64(count), nil
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ type Module interface {
|
||||
GetUserByEmailInOrg(ctx context.Context, orgID string, email string) (*types.GettableUser, error)
|
||||
GetUsersByRoleInOrg(ctx context.Context, orgID string, role types.Role) ([]*types.GettableUser, error)
|
||||
ListUsers(ctx context.Context, orgID string) ([]*types.GettableUser, error)
|
||||
UpdateUser(ctx context.Context, orgID string, id string, user *types.User) (*types.User, error)
|
||||
DeleteUser(ctx context.Context, orgID string, id string) error
|
||||
UpdateUser(ctx context.Context, orgID string, id string, user *types.User, updatedBy string) (*types.User, error)
|
||||
DeleteUser(ctx context.Context, orgID string, id string, deletedBy string) error
|
||||
|
||||
// login
|
||||
GetAuthenticatedUser(ctx context.Context, orgID, email, password, refreshToken string) (*types.User, error)
|
||||
@@ -70,6 +70,11 @@ type Module interface {
|
||||
statsreporter.StatsCollector
|
||||
}
|
||||
|
||||
type Getter interface {
|
||||
// Get gets the users based on the given id
|
||||
ListByOrgID(context.Context, valuer.UUID) ([]*types.User, error)
|
||||
}
|
||||
|
||||
type Handler interface {
|
||||
// invite
|
||||
CreateInvite(http.ResponseWriter, *http.Request)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -26,14 +26,11 @@ HAS=25
|
||||
HASANY=26
|
||||
HASALL=27
|
||||
BOOL=28
|
||||
DOLLAR_VAR=29
|
||||
CURLY_VAR=30
|
||||
SQUARE_VAR=31
|
||||
NUMBER=32
|
||||
QUOTED_TEXT=33
|
||||
KEY=34
|
||||
WS=35
|
||||
FREETEXT=36
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -26,14 +26,11 @@ HAS=25
|
||||
HASANY=26
|
||||
HASALL=27
|
||||
BOOL=28
|
||||
DOLLAR_VAR=29
|
||||
CURLY_VAR=30
|
||||
SQUARE_VAR=31
|
||||
NUMBER=32
|
||||
QUOTED_TEXT=33
|
||||
KEY=34
|
||||
WS=35
|
||||
FREETEXT=36
|
||||
NUMBER=29
|
||||
QUOTED_TEXT=30
|
||||
KEY=31
|
||||
WS=32
|
||||
FREETEXT=33
|
||||
'('=1
|
||||
')'=2
|
||||
'['=3
|
||||
|
||||
@@ -117,12 +117,6 @@ func (s *BaseFilterQueryListener) EnterValue(ctx *ValueContext) {}
|
||||
// ExitValue is called when production value is exited.
|
||||
func (s *BaseFilterQueryListener) ExitValue(ctx *ValueContext) {}
|
||||
|
||||
// EnterVariable is called when production variable is entered.
|
||||
func (s *BaseFilterQueryListener) EnterVariable(ctx *VariableContext) {}
|
||||
|
||||
// ExitVariable is called when production variable is exited.
|
||||
func (s *BaseFilterQueryListener) ExitVariable(ctx *VariableContext) {}
|
||||
|
||||
// EnterKey is called when production key is entered.
|
||||
func (s *BaseFilterQueryListener) EnterKey(ctx *KeyContext) {}
|
||||
|
||||
|
||||
@@ -72,10 +72,6 @@ func (v *BaseFilterQueryVisitor) VisitValue(ctx *ValueContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseFilterQueryVisitor) VisitVariable(ctx *VariableContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
func (v *BaseFilterQueryVisitor) VisitKey(ctx *KeyContext) interface{} {
|
||||
return v.VisitChildren(ctx)
|
||||
}
|
||||
|
||||
@@ -50,213 +50,178 @@ func filterquerylexerLexerInit() {
|
||||
"", "LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
|
||||
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "NOT_LIKE", "ILIKE", "NOT_ILIKE",
|
||||
"BETWEEN", "EXISTS", "REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR",
|
||||
"HAS", "HASANY", "HASALL", "BOOL", "DOLLAR_VAR", "CURLY_VAR", "SQUARE_VAR",
|
||||
"NUMBER", "QUOTED_TEXT", "KEY", "WS", "FREETEXT",
|
||||
"HAS", "HASANY", "HASALL", "BOOL", "NUMBER", "QUOTED_TEXT", "KEY", "WS",
|
||||
"FREETEXT",
|
||||
}
|
||||
staticData.RuleNames = []string{
|
||||
"LPAREN", "RPAREN", "LBRACK", "RBRACK", "COMMA", "EQUALS", "NOT_EQUALS",
|
||||
"NEQ", "LT", "LE", "GT", "GE", "LIKE", "NOT_LIKE", "ILIKE", "NOT_ILIKE",
|
||||
"BETWEEN", "EXISTS", "REGEXP", "CONTAINS", "IN", "NOT", "AND", "OR",
|
||||
"HAS", "HASANY", "HASALL", "BOOL", "DOLLAR_VAR", "CURLY_VAR", "SQUARE_VAR",
|
||||
"SIGN", "NUMBER", "QUOTED_TEXT", "SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS",
|
||||
"KEY", "WS", "DIGIT", "FREETEXT",
|
||||
"HAS", "HASANY", "HASALL", "BOOL", "SIGN", "NUMBER", "QUOTED_TEXT",
|
||||
"SEGMENT", "EMPTY_BRACKS", "OLD_JSON_BRACKS", "KEY", "WS", "DIGIT",
|
||||
"FREETEXT",
|
||||
}
|
||||
staticData.PredictionContextCache = antlr.NewPredictionContextCache()
|
||||
staticData.serializedATN = []int32{
|
||||
4, 0, 36, 404, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
|
||||
4, 0, 33, 334, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2,
|
||||
4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2,
|
||||
10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15,
|
||||
7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7,
|
||||
20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25,
|
||||
2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2,
|
||||
31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36,
|
||||
7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 1, 0, 1,
|
||||
0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 3,
|
||||
5, 97, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9,
|
||||
1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1,
|
||||
12, 1, 13, 1, 13, 1, 13, 1, 13, 4, 13, 124, 8, 13, 11, 13, 12, 13, 125,
|
||||
1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1,
|
||||
14, 1, 15, 1, 15, 1, 15, 1, 15, 4, 15, 143, 8, 15, 11, 15, 12, 15, 144,
|
||||
1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1,
|
||||
16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17,
|
||||
167, 8, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1,
|
||||
19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 3, 19, 184, 8, 19, 1, 20,
|
||||
1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1,
|
||||
23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25,
|
||||
1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1,
|
||||
27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 3, 27, 227,
|
||||
8, 27, 1, 28, 1, 28, 1, 28, 5, 28, 232, 8, 28, 10, 28, 12, 28, 235, 9,
|
||||
28, 1, 29, 1, 29, 1, 29, 1, 29, 5, 29, 241, 8, 29, 10, 29, 12, 29, 244,
|
||||
9, 29, 1, 29, 3, 29, 247, 8, 29, 1, 29, 1, 29, 5, 29, 251, 8, 29, 10, 29,
|
||||
12, 29, 254, 9, 29, 1, 29, 5, 29, 257, 8, 29, 10, 29, 12, 29, 260, 9, 29,
|
||||
1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 5, 30, 269, 8, 30, 10,
|
||||
30, 12, 30, 272, 9, 30, 1, 30, 3, 30, 275, 8, 30, 1, 30, 1, 30, 5, 30,
|
||||
279, 8, 30, 10, 30, 12, 30, 282, 9, 30, 1, 30, 5, 30, 285, 8, 30, 10, 30,
|
||||
12, 30, 288, 9, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 32, 3, 32, 296,
|
||||
8, 32, 1, 32, 4, 32, 299, 8, 32, 11, 32, 12, 32, 300, 1, 32, 1, 32, 5,
|
||||
32, 305, 8, 32, 10, 32, 12, 32, 308, 9, 32, 3, 32, 310, 8, 32, 1, 32, 1,
|
||||
32, 3, 32, 314, 8, 32, 1, 32, 4, 32, 317, 8, 32, 11, 32, 12, 32, 318, 3,
|
||||
32, 321, 8, 32, 1, 32, 3, 32, 324, 8, 32, 1, 32, 1, 32, 4, 32, 328, 8,
|
||||
32, 11, 32, 12, 32, 329, 1, 32, 1, 32, 3, 32, 334, 8, 32, 1, 32, 4, 32,
|
||||
337, 8, 32, 11, 32, 12, 32, 338, 3, 32, 341, 8, 32, 3, 32, 343, 8, 32,
|
||||
1, 33, 1, 33, 1, 33, 1, 33, 5, 33, 349, 8, 33, 10, 33, 12, 33, 352, 9,
|
||||
33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 5, 33, 359, 8, 33, 10, 33, 12, 33,
|
||||
362, 9, 33, 1, 33, 3, 33, 365, 8, 33, 1, 34, 1, 34, 5, 34, 369, 8, 34,
|
||||
10, 34, 12, 34, 372, 9, 34, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1,
|
||||
36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 5, 37, 386, 8, 37, 10, 37, 12, 37,
|
||||
389, 9, 37, 1, 38, 4, 38, 392, 8, 38, 11, 38, 12, 38, 393, 1, 38, 1, 38,
|
||||
1, 39, 1, 39, 1, 40, 4, 40, 401, 8, 40, 11, 40, 12, 40, 402, 0, 0, 41,
|
||||
1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11,
|
||||
23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20,
|
||||
41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 29,
|
||||
59, 30, 61, 31, 63, 0, 65, 32, 67, 33, 69, 0, 71, 0, 73, 0, 75, 34, 77,
|
||||
35, 79, 0, 81, 36, 1, 0, 32, 2, 0, 76, 76, 108, 108, 2, 0, 73, 73, 105,
|
||||
105, 2, 0, 75, 75, 107, 107, 2, 0, 69, 69, 101, 101, 2, 0, 78, 78, 110,
|
||||
110, 2, 0, 79, 79, 111, 111, 2, 0, 84, 84, 116, 116, 2, 0, 9, 9, 32, 32,
|
||||
2, 0, 66, 66, 98, 98, 2, 0, 87, 87, 119, 119, 2, 0, 88, 88, 120, 120, 2,
|
||||
0, 83, 83, 115, 115, 2, 0, 82, 82, 114, 114, 2, 0, 71, 71, 103, 103, 2,
|
||||
0, 80, 80, 112, 112, 2, 0, 67, 67, 99, 99, 2, 0, 65, 65, 97, 97, 2, 0,
|
||||
68, 68, 100, 100, 2, 0, 72, 72, 104, 104, 2, 0, 89, 89, 121, 121, 2, 0,
|
||||
85, 85, 117, 117, 2, 0, 70, 70, 102, 102, 3, 0, 65, 90, 95, 95, 97, 122,
|
||||
5, 0, 46, 46, 48, 57, 65, 90, 95, 95, 97, 122, 2, 0, 43, 43, 45, 45, 2,
|
||||
0, 34, 34, 92, 92, 2, 0, 39, 39, 92, 92, 2, 0, 65, 90, 97, 122, 5, 0, 45,
|
||||
45, 48, 58, 65, 90, 95, 95, 97, 122, 3, 0, 9, 10, 13, 13, 32, 32, 1, 0,
|
||||
48, 57, 8, 0, 9, 10, 13, 13, 32, 34, 39, 41, 44, 44, 60, 62, 91, 91, 93,
|
||||
93, 437, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1,
|
||||
0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15,
|
||||
1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0,
|
||||
23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0,
|
||||
0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0,
|
||||
0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0,
|
||||
0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1,
|
||||
0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61,
|
||||
1, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 0,
|
||||
77, 1, 0, 0, 0, 0, 81, 1, 0, 0, 0, 1, 83, 1, 0, 0, 0, 3, 85, 1, 0, 0, 0,
|
||||
5, 87, 1, 0, 0, 0, 7, 89, 1, 0, 0, 0, 9, 91, 1, 0, 0, 0, 11, 96, 1, 0,
|
||||
0, 0, 13, 98, 1, 0, 0, 0, 15, 101, 1, 0, 0, 0, 17, 104, 1, 0, 0, 0, 19,
|
||||
106, 1, 0, 0, 0, 21, 109, 1, 0, 0, 0, 23, 111, 1, 0, 0, 0, 25, 114, 1,
|
||||
0, 0, 0, 27, 119, 1, 0, 0, 0, 29, 132, 1, 0, 0, 0, 31, 138, 1, 0, 0, 0,
|
||||
33, 152, 1, 0, 0, 0, 35, 160, 1, 0, 0, 0, 37, 168, 1, 0, 0, 0, 39, 175,
|
||||
1, 0, 0, 0, 41, 185, 1, 0, 0, 0, 43, 188, 1, 0, 0, 0, 45, 192, 1, 0, 0,
|
||||
0, 47, 196, 1, 0, 0, 0, 49, 199, 1, 0, 0, 0, 51, 203, 1, 0, 0, 0, 53, 210,
|
||||
1, 0, 0, 0, 55, 226, 1, 0, 0, 0, 57, 228, 1, 0, 0, 0, 59, 236, 1, 0, 0,
|
||||
0, 61, 264, 1, 0, 0, 0, 63, 292, 1, 0, 0, 0, 65, 342, 1, 0, 0, 0, 67, 364,
|
||||
1, 0, 0, 0, 69, 366, 1, 0, 0, 0, 71, 373, 1, 0, 0, 0, 73, 376, 1, 0, 0,
|
||||
0, 75, 380, 1, 0, 0, 0, 77, 391, 1, 0, 0, 0, 79, 397, 1, 0, 0, 0, 81, 400,
|
||||
1, 0, 0, 0, 83, 84, 5, 40, 0, 0, 84, 2, 1, 0, 0, 0, 85, 86, 5, 41, 0, 0,
|
||||
86, 4, 1, 0, 0, 0, 87, 88, 5, 91, 0, 0, 88, 6, 1, 0, 0, 0, 89, 90, 5, 93,
|
||||
0, 0, 90, 8, 1, 0, 0, 0, 91, 92, 5, 44, 0, 0, 92, 10, 1, 0, 0, 0, 93, 97,
|
||||
5, 61, 0, 0, 94, 95, 5, 61, 0, 0, 95, 97, 5, 61, 0, 0, 96, 93, 1, 0, 0,
|
||||
0, 96, 94, 1, 0, 0, 0, 97, 12, 1, 0, 0, 0, 98, 99, 5, 33, 0, 0, 99, 100,
|
||||
5, 61, 0, 0, 100, 14, 1, 0, 0, 0, 101, 102, 5, 60, 0, 0, 102, 103, 5, 62,
|
||||
0, 0, 103, 16, 1, 0, 0, 0, 104, 105, 5, 60, 0, 0, 105, 18, 1, 0, 0, 0,
|
||||
106, 107, 5, 60, 0, 0, 107, 108, 5, 61, 0, 0, 108, 20, 1, 0, 0, 0, 109,
|
||||
110, 5, 62, 0, 0, 110, 22, 1, 0, 0, 0, 111, 112, 5, 62, 0, 0, 112, 113,
|
||||
5, 61, 0, 0, 113, 24, 1, 0, 0, 0, 114, 115, 7, 0, 0, 0, 115, 116, 7, 1,
|
||||
0, 0, 116, 117, 7, 2, 0, 0, 117, 118, 7, 3, 0, 0, 118, 26, 1, 0, 0, 0,
|
||||
119, 120, 7, 4, 0, 0, 120, 121, 7, 5, 0, 0, 121, 123, 7, 6, 0, 0, 122,
|
||||
124, 7, 7, 0, 0, 123, 122, 1, 0, 0, 0, 124, 125, 1, 0, 0, 0, 125, 123,
|
||||
1, 0, 0, 0, 125, 126, 1, 0, 0, 0, 126, 127, 1, 0, 0, 0, 127, 128, 7, 0,
|
||||
0, 0, 128, 129, 7, 1, 0, 0, 129, 130, 7, 2, 0, 0, 130, 131, 7, 3, 0, 0,
|
||||
131, 28, 1, 0, 0, 0, 132, 133, 7, 1, 0, 0, 133, 134, 7, 0, 0, 0, 134, 135,
|
||||
7, 1, 0, 0, 135, 136, 7, 2, 0, 0, 136, 137, 7, 3, 0, 0, 137, 30, 1, 0,
|
||||
0, 0, 138, 139, 7, 4, 0, 0, 139, 140, 7, 5, 0, 0, 140, 142, 7, 6, 0, 0,
|
||||
141, 143, 7, 7, 0, 0, 142, 141, 1, 0, 0, 0, 143, 144, 1, 0, 0, 0, 144,
|
||||
142, 1, 0, 0, 0, 144, 145, 1, 0, 0, 0, 145, 146, 1, 0, 0, 0, 146, 147,
|
||||
7, 1, 0, 0, 147, 148, 7, 0, 0, 0, 148, 149, 7, 1, 0, 0, 149, 150, 7, 2,
|
||||
0, 0, 150, 151, 7, 3, 0, 0, 151, 32, 1, 0, 0, 0, 152, 153, 7, 8, 0, 0,
|
||||
153, 154, 7, 3, 0, 0, 154, 155, 7, 6, 0, 0, 155, 156, 7, 9, 0, 0, 156,
|
||||
157, 7, 3, 0, 0, 157, 158, 7, 3, 0, 0, 158, 159, 7, 4, 0, 0, 159, 34, 1,
|
||||
0, 0, 0, 160, 161, 7, 3, 0, 0, 161, 162, 7, 10, 0, 0, 162, 163, 7, 1, 0,
|
||||
0, 163, 164, 7, 11, 0, 0, 164, 166, 7, 6, 0, 0, 165, 167, 7, 11, 0, 0,
|
||||
166, 165, 1, 0, 0, 0, 166, 167, 1, 0, 0, 0, 167, 36, 1, 0, 0, 0, 168, 169,
|
||||
7, 12, 0, 0, 169, 170, 7, 3, 0, 0, 170, 171, 7, 13, 0, 0, 171, 172, 7,
|
||||
3, 0, 0, 172, 173, 7, 10, 0, 0, 173, 174, 7, 14, 0, 0, 174, 38, 1, 0, 0,
|
||||
0, 175, 176, 7, 15, 0, 0, 176, 177, 7, 5, 0, 0, 177, 178, 7, 4, 0, 0, 178,
|
||||
179, 7, 6, 0, 0, 179, 180, 7, 16, 0, 0, 180, 181, 7, 1, 0, 0, 181, 183,
|
||||
7, 4, 0, 0, 182, 184, 7, 11, 0, 0, 183, 182, 1, 0, 0, 0, 183, 184, 1, 0,
|
||||
0, 0, 184, 40, 1, 0, 0, 0, 185, 186, 7, 1, 0, 0, 186, 187, 7, 4, 0, 0,
|
||||
187, 42, 1, 0, 0, 0, 188, 189, 7, 4, 0, 0, 189, 190, 7, 5, 0, 0, 190, 191,
|
||||
7, 6, 0, 0, 191, 44, 1, 0, 0, 0, 192, 193, 7, 16, 0, 0, 193, 194, 7, 4,
|
||||
0, 0, 194, 195, 7, 17, 0, 0, 195, 46, 1, 0, 0, 0, 196, 197, 7, 5, 0, 0,
|
||||
197, 198, 7, 12, 0, 0, 198, 48, 1, 0, 0, 0, 199, 200, 7, 18, 0, 0, 200,
|
||||
201, 7, 16, 0, 0, 201, 202, 7, 11, 0, 0, 202, 50, 1, 0, 0, 0, 203, 204,
|
||||
7, 18, 0, 0, 204, 205, 7, 16, 0, 0, 205, 206, 7, 11, 0, 0, 206, 207, 7,
|
||||
16, 0, 0, 207, 208, 7, 4, 0, 0, 208, 209, 7, 19, 0, 0, 209, 52, 1, 0, 0,
|
||||
0, 210, 211, 7, 18, 0, 0, 211, 212, 7, 16, 0, 0, 212, 213, 7, 11, 0, 0,
|
||||
213, 214, 7, 16, 0, 0, 214, 215, 7, 0, 0, 0, 215, 216, 7, 0, 0, 0, 216,
|
||||
54, 1, 0, 0, 0, 217, 218, 7, 6, 0, 0, 218, 219, 7, 12, 0, 0, 219, 220,
|
||||
7, 20, 0, 0, 220, 227, 7, 3, 0, 0, 221, 222, 7, 21, 0, 0, 222, 223, 7,
|
||||
16, 0, 0, 223, 224, 7, 0, 0, 0, 224, 225, 7, 11, 0, 0, 225, 227, 7, 3,
|
||||
0, 0, 226, 217, 1, 0, 0, 0, 226, 221, 1, 0, 0, 0, 227, 56, 1, 0, 0, 0,
|
||||
228, 229, 5, 36, 0, 0, 229, 233, 7, 22, 0, 0, 230, 232, 7, 23, 0, 0, 231,
|
||||
230, 1, 0, 0, 0, 232, 235, 1, 0, 0, 0, 233, 231, 1, 0, 0, 0, 233, 234,
|
||||
1, 0, 0, 0, 234, 58, 1, 0, 0, 0, 235, 233, 1, 0, 0, 0, 236, 237, 5, 123,
|
||||
0, 0, 237, 238, 5, 123, 0, 0, 238, 242, 1, 0, 0, 0, 239, 241, 7, 7, 0,
|
||||
0, 240, 239, 1, 0, 0, 0, 241, 244, 1, 0, 0, 0, 242, 240, 1, 0, 0, 0, 242,
|
||||
243, 1, 0, 0, 0, 243, 246, 1, 0, 0, 0, 244, 242, 1, 0, 0, 0, 245, 247,
|
||||
5, 46, 0, 0, 246, 245, 1, 0, 0, 0, 246, 247, 1, 0, 0, 0, 247, 248, 1, 0,
|
||||
0, 0, 248, 252, 7, 22, 0, 0, 249, 251, 7, 23, 0, 0, 250, 249, 1, 0, 0,
|
||||
0, 251, 254, 1, 0, 0, 0, 252, 250, 1, 0, 0, 0, 252, 253, 1, 0, 0, 0, 253,
|
||||
258, 1, 0, 0, 0, 254, 252, 1, 0, 0, 0, 255, 257, 7, 7, 0, 0, 256, 255,
|
||||
1, 0, 0, 0, 257, 260, 1, 0, 0, 0, 258, 256, 1, 0, 0, 0, 258, 259, 1, 0,
|
||||
0, 0, 259, 261, 1, 0, 0, 0, 260, 258, 1, 0, 0, 0, 261, 262, 5, 125, 0,
|
||||
0, 262, 263, 5, 125, 0, 0, 263, 60, 1, 0, 0, 0, 264, 265, 5, 91, 0, 0,
|
||||
265, 266, 5, 91, 0, 0, 266, 270, 1, 0, 0, 0, 267, 269, 7, 7, 0, 0, 268,
|
||||
267, 1, 0, 0, 0, 269, 272, 1, 0, 0, 0, 270, 268, 1, 0, 0, 0, 270, 271,
|
||||
1, 0, 0, 0, 271, 274, 1, 0, 0, 0, 272, 270, 1, 0, 0, 0, 273, 275, 5, 46,
|
||||
0, 0, 274, 273, 1, 0, 0, 0, 274, 275, 1, 0, 0, 0, 275, 276, 1, 0, 0, 0,
|
||||
276, 280, 7, 22, 0, 0, 277, 279, 7, 23, 0, 0, 278, 277, 1, 0, 0, 0, 279,
|
||||
282, 1, 0, 0, 0, 280, 278, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 286,
|
||||
1, 0, 0, 0, 282, 280, 1, 0, 0, 0, 283, 285, 7, 7, 0, 0, 284, 283, 1, 0,
|
||||
0, 0, 285, 288, 1, 0, 0, 0, 286, 284, 1, 0, 0, 0, 286, 287, 1, 0, 0, 0,
|
||||
287, 289, 1, 0, 0, 0, 288, 286, 1, 0, 0, 0, 289, 290, 5, 93, 0, 0, 290,
|
||||
291, 5, 93, 0, 0, 291, 62, 1, 0, 0, 0, 292, 293, 7, 24, 0, 0, 293, 64,
|
||||
1, 0, 0, 0, 294, 296, 3, 63, 31, 0, 295, 294, 1, 0, 0, 0, 295, 296, 1,
|
||||
0, 0, 0, 296, 298, 1, 0, 0, 0, 297, 299, 3, 79, 39, 0, 298, 297, 1, 0,
|
||||
0, 0, 299, 300, 1, 0, 0, 0, 300, 298, 1, 0, 0, 0, 300, 301, 1, 0, 0, 0,
|
||||
301, 309, 1, 0, 0, 0, 302, 306, 5, 46, 0, 0, 303, 305, 3, 79, 39, 0, 304,
|
||||
303, 1, 0, 0, 0, 305, 308, 1, 0, 0, 0, 306, 304, 1, 0, 0, 0, 306, 307,
|
||||
1, 0, 0, 0, 307, 310, 1, 0, 0, 0, 308, 306, 1, 0, 0, 0, 309, 302, 1, 0,
|
||||
0, 0, 309, 310, 1, 0, 0, 0, 310, 320, 1, 0, 0, 0, 311, 313, 7, 3, 0, 0,
|
||||
312, 314, 3, 63, 31, 0, 313, 312, 1, 0, 0, 0, 313, 314, 1, 0, 0, 0, 314,
|
||||
316, 1, 0, 0, 0, 315, 317, 3, 79, 39, 0, 316, 315, 1, 0, 0, 0, 317, 318,
|
||||
1, 0, 0, 0, 318, 316, 1, 0, 0, 0, 318, 319, 1, 0, 0, 0, 319, 321, 1, 0,
|
||||
0, 0, 320, 311, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 343, 1, 0, 0, 0,
|
||||
322, 324, 3, 63, 31, 0, 323, 322, 1, 0, 0, 0, 323, 324, 1, 0, 0, 0, 324,
|
||||
325, 1, 0, 0, 0, 325, 327, 5, 46, 0, 0, 326, 328, 3, 79, 39, 0, 327, 326,
|
||||
1, 0, 0, 0, 328, 329, 1, 0, 0, 0, 329, 327, 1, 0, 0, 0, 329, 330, 1, 0,
|
||||
0, 0, 330, 340, 1, 0, 0, 0, 331, 333, 7, 3, 0, 0, 332, 334, 3, 63, 31,
|
||||
0, 333, 332, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 336, 1, 0, 0, 0, 335,
|
||||
337, 3, 79, 39, 0, 336, 335, 1, 0, 0, 0, 337, 338, 1, 0, 0, 0, 338, 336,
|
||||
1, 0, 0, 0, 338, 339, 1, 0, 0, 0, 339, 341, 1, 0, 0, 0, 340, 331, 1, 0,
|
||||
0, 0, 340, 341, 1, 0, 0, 0, 341, 343, 1, 0, 0, 0, 342, 295, 1, 0, 0, 0,
|
||||
342, 323, 1, 0, 0, 0, 343, 66, 1, 0, 0, 0, 344, 350, 5, 34, 0, 0, 345,
|
||||
349, 8, 25, 0, 0, 346, 347, 5, 92, 0, 0, 347, 349, 9, 0, 0, 0, 348, 345,
|
||||
1, 0, 0, 0, 348, 346, 1, 0, 0, 0, 349, 352, 1, 0, 0, 0, 350, 348, 1, 0,
|
||||
0, 0, 350, 351, 1, 0, 0, 0, 351, 353, 1, 0, 0, 0, 352, 350, 1, 0, 0, 0,
|
||||
353, 365, 5, 34, 0, 0, 354, 360, 5, 39, 0, 0, 355, 359, 8, 26, 0, 0, 356,
|
||||
357, 5, 92, 0, 0, 357, 359, 9, 0, 0, 0, 358, 355, 1, 0, 0, 0, 358, 356,
|
||||
1, 0, 0, 0, 359, 362, 1, 0, 0, 0, 360, 358, 1, 0, 0, 0, 360, 361, 1, 0,
|
||||
0, 0, 361, 363, 1, 0, 0, 0, 362, 360, 1, 0, 0, 0, 363, 365, 5, 39, 0, 0,
|
||||
364, 344, 1, 0, 0, 0, 364, 354, 1, 0, 0, 0, 365, 68, 1, 0, 0, 0, 366, 370,
|
||||
7, 27, 0, 0, 367, 369, 7, 28, 0, 0, 368, 367, 1, 0, 0, 0, 369, 372, 1,
|
||||
0, 0, 0, 370, 368, 1, 0, 0, 0, 370, 371, 1, 0, 0, 0, 371, 70, 1, 0, 0,
|
||||
0, 372, 370, 1, 0, 0, 0, 373, 374, 5, 91, 0, 0, 374, 375, 5, 93, 0, 0,
|
||||
375, 72, 1, 0, 0, 0, 376, 377, 5, 91, 0, 0, 377, 378, 5, 42, 0, 0, 378,
|
||||
379, 5, 93, 0, 0, 379, 74, 1, 0, 0, 0, 380, 387, 3, 69, 34, 0, 381, 382,
|
||||
5, 46, 0, 0, 382, 386, 3, 69, 34, 0, 383, 386, 3, 71, 35, 0, 384, 386,
|
||||
3, 73, 36, 0, 385, 381, 1, 0, 0, 0, 385, 383, 1, 0, 0, 0, 385, 384, 1,
|
||||
0, 0, 0, 386, 389, 1, 0, 0, 0, 387, 385, 1, 0, 0, 0, 387, 388, 1, 0, 0,
|
||||
0, 388, 76, 1, 0, 0, 0, 389, 387, 1, 0, 0, 0, 390, 392, 7, 29, 0, 0, 391,
|
||||
390, 1, 0, 0, 0, 392, 393, 1, 0, 0, 0, 393, 391, 1, 0, 0, 0, 393, 394,
|
||||
1, 0, 0, 0, 394, 395, 1, 0, 0, 0, 395, 396, 6, 38, 0, 0, 396, 78, 1, 0,
|
||||
0, 0, 397, 398, 7, 30, 0, 0, 398, 80, 1, 0, 0, 0, 399, 401, 8, 31, 0, 0,
|
||||
400, 399, 1, 0, 0, 0, 401, 402, 1, 0, 0, 0, 402, 400, 1, 0, 0, 0, 402,
|
||||
403, 1, 0, 0, 0, 403, 82, 1, 0, 0, 0, 39, 0, 96, 125, 144, 166, 183, 226,
|
||||
233, 242, 246, 252, 258, 270, 274, 280, 286, 295, 300, 306, 309, 313, 318,
|
||||
320, 323, 329, 333, 338, 340, 342, 348, 350, 358, 360, 364, 370, 385, 387,
|
||||
393, 402, 1, 6, 0, 0,
|
||||
7, 36, 2, 37, 7, 37, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1,
|
||||
4, 1, 4, 1, 5, 1, 5, 1, 5, 3, 5, 91, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7,
|
||||
1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11,
|
||||
1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 4, 13, 118,
|
||||
8, 13, 11, 13, 12, 13, 119, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1,
|
||||
14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 4, 15, 137,
|
||||
8, 15, 11, 15, 12, 15, 138, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1,
|
||||
16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17,
|
||||
1, 17, 1, 17, 1, 17, 3, 17, 161, 8, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1,
|
||||
18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19,
|
||||
3, 19, 178, 8, 19, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1,
|
||||
22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24,
|
||||
1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1,
|
||||
26, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27,
|
||||
1, 27, 1, 27, 3, 27, 221, 8, 27, 1, 28, 1, 28, 1, 29, 3, 29, 226, 8, 29,
|
||||
1, 29, 4, 29, 229, 8, 29, 11, 29, 12, 29, 230, 1, 29, 1, 29, 5, 29, 235,
|
||||
8, 29, 10, 29, 12, 29, 238, 9, 29, 3, 29, 240, 8, 29, 1, 29, 1, 29, 3,
|
||||
29, 244, 8, 29, 1, 29, 4, 29, 247, 8, 29, 11, 29, 12, 29, 248, 3, 29, 251,
|
||||
8, 29, 1, 29, 3, 29, 254, 8, 29, 1, 29, 1, 29, 4, 29, 258, 8, 29, 11, 29,
|
||||
12, 29, 259, 1, 29, 1, 29, 3, 29, 264, 8, 29, 1, 29, 4, 29, 267, 8, 29,
|
||||
11, 29, 12, 29, 268, 3, 29, 271, 8, 29, 3, 29, 273, 8, 29, 1, 30, 1, 30,
|
||||
1, 30, 1, 30, 5, 30, 279, 8, 30, 10, 30, 12, 30, 282, 9, 30, 1, 30, 1,
|
||||
30, 1, 30, 1, 30, 1, 30, 5, 30, 289, 8, 30, 10, 30, 12, 30, 292, 9, 30,
|
||||
1, 30, 3, 30, 295, 8, 30, 1, 31, 1, 31, 5, 31, 299, 8, 31, 10, 31, 12,
|
||||
31, 302, 9, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34,
|
||||
1, 34, 1, 34, 1, 34, 1, 34, 5, 34, 316, 8, 34, 10, 34, 12, 34, 319, 9,
|
||||
34, 1, 35, 4, 35, 322, 8, 35, 11, 35, 12, 35, 323, 1, 35, 1, 35, 1, 36,
|
||||
1, 36, 1, 37, 4, 37, 331, 8, 37, 11, 37, 12, 37, 332, 0, 0, 38, 1, 1, 3,
|
||||
2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12,
|
||||
25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20, 41, 21,
|
||||
43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 0, 59, 29,
|
||||
61, 30, 63, 0, 65, 0, 67, 0, 69, 31, 71, 32, 73, 0, 75, 33, 1, 0, 30, 2,
|
||||
0, 76, 76, 108, 108, 2, 0, 73, 73, 105, 105, 2, 0, 75, 75, 107, 107, 2,
|
||||
0, 69, 69, 101, 101, 2, 0, 78, 78, 110, 110, 2, 0, 79, 79, 111, 111, 2,
|
||||
0, 84, 84, 116, 116, 2, 0, 9, 9, 32, 32, 2, 0, 66, 66, 98, 98, 2, 0, 87,
|
||||
87, 119, 119, 2, 0, 88, 88, 120, 120, 2, 0, 83, 83, 115, 115, 2, 0, 82,
|
||||
82, 114, 114, 2, 0, 71, 71, 103, 103, 2, 0, 80, 80, 112, 112, 2, 0, 67,
|
||||
67, 99, 99, 2, 0, 65, 65, 97, 97, 2, 0, 68, 68, 100, 100, 2, 0, 72, 72,
|
||||
104, 104, 2, 0, 89, 89, 121, 121, 2, 0, 85, 85, 117, 117, 2, 0, 70, 70,
|
||||
102, 102, 2, 0, 43, 43, 45, 45, 2, 0, 34, 34, 92, 92, 2, 0, 39, 39, 92,
|
||||
92, 2, 0, 65, 90, 97, 122, 5, 0, 45, 45, 48, 58, 65, 90, 95, 95, 97, 122,
|
||||
3, 0, 9, 10, 13, 13, 32, 32, 1, 0, 48, 57, 8, 0, 9, 10, 13, 13, 32, 34,
|
||||
39, 41, 44, 44, 60, 62, 91, 91, 93, 93, 358, 0, 1, 1, 0, 0, 0, 0, 3, 1,
|
||||
0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1,
|
||||
0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19,
|
||||
1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0,
|
||||
27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0,
|
||||
0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0,
|
||||
0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0,
|
||||
0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 59, 1,
|
||||
0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 75,
|
||||
1, 0, 0, 0, 1, 77, 1, 0, 0, 0, 3, 79, 1, 0, 0, 0, 5, 81, 1, 0, 0, 0, 7,
|
||||
83, 1, 0, 0, 0, 9, 85, 1, 0, 0, 0, 11, 90, 1, 0, 0, 0, 13, 92, 1, 0, 0,
|
||||
0, 15, 95, 1, 0, 0, 0, 17, 98, 1, 0, 0, 0, 19, 100, 1, 0, 0, 0, 21, 103,
|
||||
1, 0, 0, 0, 23, 105, 1, 0, 0, 0, 25, 108, 1, 0, 0, 0, 27, 113, 1, 0, 0,
|
||||
0, 29, 126, 1, 0, 0, 0, 31, 132, 1, 0, 0, 0, 33, 146, 1, 0, 0, 0, 35, 154,
|
||||
1, 0, 0, 0, 37, 162, 1, 0, 0, 0, 39, 169, 1, 0, 0, 0, 41, 179, 1, 0, 0,
|
||||
0, 43, 182, 1, 0, 0, 0, 45, 186, 1, 0, 0, 0, 47, 190, 1, 0, 0, 0, 49, 193,
|
||||
1, 0, 0, 0, 51, 197, 1, 0, 0, 0, 53, 204, 1, 0, 0, 0, 55, 220, 1, 0, 0,
|
||||
0, 57, 222, 1, 0, 0, 0, 59, 272, 1, 0, 0, 0, 61, 294, 1, 0, 0, 0, 63, 296,
|
||||
1, 0, 0, 0, 65, 303, 1, 0, 0, 0, 67, 306, 1, 0, 0, 0, 69, 310, 1, 0, 0,
|
||||
0, 71, 321, 1, 0, 0, 0, 73, 327, 1, 0, 0, 0, 75, 330, 1, 0, 0, 0, 77, 78,
|
||||
5, 40, 0, 0, 78, 2, 1, 0, 0, 0, 79, 80, 5, 41, 0, 0, 80, 4, 1, 0, 0, 0,
|
||||
81, 82, 5, 91, 0, 0, 82, 6, 1, 0, 0, 0, 83, 84, 5, 93, 0, 0, 84, 8, 1,
|
||||
0, 0, 0, 85, 86, 5, 44, 0, 0, 86, 10, 1, 0, 0, 0, 87, 91, 5, 61, 0, 0,
|
||||
88, 89, 5, 61, 0, 0, 89, 91, 5, 61, 0, 0, 90, 87, 1, 0, 0, 0, 90, 88, 1,
|
||||
0, 0, 0, 91, 12, 1, 0, 0, 0, 92, 93, 5, 33, 0, 0, 93, 94, 5, 61, 0, 0,
|
||||
94, 14, 1, 0, 0, 0, 95, 96, 5, 60, 0, 0, 96, 97, 5, 62, 0, 0, 97, 16, 1,
|
||||
0, 0, 0, 98, 99, 5, 60, 0, 0, 99, 18, 1, 0, 0, 0, 100, 101, 5, 60, 0, 0,
|
||||
101, 102, 5, 61, 0, 0, 102, 20, 1, 0, 0, 0, 103, 104, 5, 62, 0, 0, 104,
|
||||
22, 1, 0, 0, 0, 105, 106, 5, 62, 0, 0, 106, 107, 5, 61, 0, 0, 107, 24,
|
||||
1, 0, 0, 0, 108, 109, 7, 0, 0, 0, 109, 110, 7, 1, 0, 0, 110, 111, 7, 2,
|
||||
0, 0, 111, 112, 7, 3, 0, 0, 112, 26, 1, 0, 0, 0, 113, 114, 7, 4, 0, 0,
|
||||
114, 115, 7, 5, 0, 0, 115, 117, 7, 6, 0, 0, 116, 118, 7, 7, 0, 0, 117,
|
||||
116, 1, 0, 0, 0, 118, 119, 1, 0, 0, 0, 119, 117, 1, 0, 0, 0, 119, 120,
|
||||
1, 0, 0, 0, 120, 121, 1, 0, 0, 0, 121, 122, 7, 0, 0, 0, 122, 123, 7, 1,
|
||||
0, 0, 123, 124, 7, 2, 0, 0, 124, 125, 7, 3, 0, 0, 125, 28, 1, 0, 0, 0,
|
||||
126, 127, 7, 1, 0, 0, 127, 128, 7, 0, 0, 0, 128, 129, 7, 1, 0, 0, 129,
|
||||
130, 7, 2, 0, 0, 130, 131, 7, 3, 0, 0, 131, 30, 1, 0, 0, 0, 132, 133, 7,
|
||||
4, 0, 0, 133, 134, 7, 5, 0, 0, 134, 136, 7, 6, 0, 0, 135, 137, 7, 7, 0,
|
||||
0, 136, 135, 1, 0, 0, 0, 137, 138, 1, 0, 0, 0, 138, 136, 1, 0, 0, 0, 138,
|
||||
139, 1, 0, 0, 0, 139, 140, 1, 0, 0, 0, 140, 141, 7, 1, 0, 0, 141, 142,
|
||||
7, 0, 0, 0, 142, 143, 7, 1, 0, 0, 143, 144, 7, 2, 0, 0, 144, 145, 7, 3,
|
||||
0, 0, 145, 32, 1, 0, 0, 0, 146, 147, 7, 8, 0, 0, 147, 148, 7, 3, 0, 0,
|
||||
148, 149, 7, 6, 0, 0, 149, 150, 7, 9, 0, 0, 150, 151, 7, 3, 0, 0, 151,
|
||||
152, 7, 3, 0, 0, 152, 153, 7, 4, 0, 0, 153, 34, 1, 0, 0, 0, 154, 155, 7,
|
||||
3, 0, 0, 155, 156, 7, 10, 0, 0, 156, 157, 7, 1, 0, 0, 157, 158, 7, 11,
|
||||
0, 0, 158, 160, 7, 6, 0, 0, 159, 161, 7, 11, 0, 0, 160, 159, 1, 0, 0, 0,
|
||||
160, 161, 1, 0, 0, 0, 161, 36, 1, 0, 0, 0, 162, 163, 7, 12, 0, 0, 163,
|
||||
164, 7, 3, 0, 0, 164, 165, 7, 13, 0, 0, 165, 166, 7, 3, 0, 0, 166, 167,
|
||||
7, 10, 0, 0, 167, 168, 7, 14, 0, 0, 168, 38, 1, 0, 0, 0, 169, 170, 7, 15,
|
||||
0, 0, 170, 171, 7, 5, 0, 0, 171, 172, 7, 4, 0, 0, 172, 173, 7, 6, 0, 0,
|
||||
173, 174, 7, 16, 0, 0, 174, 175, 7, 1, 0, 0, 175, 177, 7, 4, 0, 0, 176,
|
||||
178, 7, 11, 0, 0, 177, 176, 1, 0, 0, 0, 177, 178, 1, 0, 0, 0, 178, 40,
|
||||
1, 0, 0, 0, 179, 180, 7, 1, 0, 0, 180, 181, 7, 4, 0, 0, 181, 42, 1, 0,
|
||||
0, 0, 182, 183, 7, 4, 0, 0, 183, 184, 7, 5, 0, 0, 184, 185, 7, 6, 0, 0,
|
||||
185, 44, 1, 0, 0, 0, 186, 187, 7, 16, 0, 0, 187, 188, 7, 4, 0, 0, 188,
|
||||
189, 7, 17, 0, 0, 189, 46, 1, 0, 0, 0, 190, 191, 7, 5, 0, 0, 191, 192,
|
||||
7, 12, 0, 0, 192, 48, 1, 0, 0, 0, 193, 194, 7, 18, 0, 0, 194, 195, 7, 16,
|
||||
0, 0, 195, 196, 7, 11, 0, 0, 196, 50, 1, 0, 0, 0, 197, 198, 7, 18, 0, 0,
|
||||
198, 199, 7, 16, 0, 0, 199, 200, 7, 11, 0, 0, 200, 201, 7, 16, 0, 0, 201,
|
||||
202, 7, 4, 0, 0, 202, 203, 7, 19, 0, 0, 203, 52, 1, 0, 0, 0, 204, 205,
|
||||
7, 18, 0, 0, 205, 206, 7, 16, 0, 0, 206, 207, 7, 11, 0, 0, 207, 208, 7,
|
||||
16, 0, 0, 208, 209, 7, 0, 0, 0, 209, 210, 7, 0, 0, 0, 210, 54, 1, 0, 0,
|
||||
0, 211, 212, 7, 6, 0, 0, 212, 213, 7, 12, 0, 0, 213, 214, 7, 20, 0, 0,
|
||||
214, 221, 7, 3, 0, 0, 215, 216, 7, 21, 0, 0, 216, 217, 7, 16, 0, 0, 217,
|
||||
218, 7, 0, 0, 0, 218, 219, 7, 11, 0, 0, 219, 221, 7, 3, 0, 0, 220, 211,
|
||||
1, 0, 0, 0, 220, 215, 1, 0, 0, 0, 221, 56, 1, 0, 0, 0, 222, 223, 7, 22,
|
||||
0, 0, 223, 58, 1, 0, 0, 0, 224, 226, 3, 57, 28, 0, 225, 224, 1, 0, 0, 0,
|
||||
225, 226, 1, 0, 0, 0, 226, 228, 1, 0, 0, 0, 227, 229, 3, 73, 36, 0, 228,
|
||||
227, 1, 0, 0, 0, 229, 230, 1, 0, 0, 0, 230, 228, 1, 0, 0, 0, 230, 231,
|
||||
1, 0, 0, 0, 231, 239, 1, 0, 0, 0, 232, 236, 5, 46, 0, 0, 233, 235, 3, 73,
|
||||
36, 0, 234, 233, 1, 0, 0, 0, 235, 238, 1, 0, 0, 0, 236, 234, 1, 0, 0, 0,
|
||||
236, 237, 1, 0, 0, 0, 237, 240, 1, 0, 0, 0, 238, 236, 1, 0, 0, 0, 239,
|
||||
232, 1, 0, 0, 0, 239, 240, 1, 0, 0, 0, 240, 250, 1, 0, 0, 0, 241, 243,
|
||||
7, 3, 0, 0, 242, 244, 3, 57, 28, 0, 243, 242, 1, 0, 0, 0, 243, 244, 1,
|
||||
0, 0, 0, 244, 246, 1, 0, 0, 0, 245, 247, 3, 73, 36, 0, 246, 245, 1, 0,
|
||||
0, 0, 247, 248, 1, 0, 0, 0, 248, 246, 1, 0, 0, 0, 248, 249, 1, 0, 0, 0,
|
||||
249, 251, 1, 0, 0, 0, 250, 241, 1, 0, 0, 0, 250, 251, 1, 0, 0, 0, 251,
|
||||
273, 1, 0, 0, 0, 252, 254, 3, 57, 28, 0, 253, 252, 1, 0, 0, 0, 253, 254,
|
||||
1, 0, 0, 0, 254, 255, 1, 0, 0, 0, 255, 257, 5, 46, 0, 0, 256, 258, 3, 73,
|
||||
36, 0, 257, 256, 1, 0, 0, 0, 258, 259, 1, 0, 0, 0, 259, 257, 1, 0, 0, 0,
|
||||
259, 260, 1, 0, 0, 0, 260, 270, 1, 0, 0, 0, 261, 263, 7, 3, 0, 0, 262,
|
||||
264, 3, 57, 28, 0, 263, 262, 1, 0, 0, 0, 263, 264, 1, 0, 0, 0, 264, 266,
|
||||
1, 0, 0, 0, 265, 267, 3, 73, 36, 0, 266, 265, 1, 0, 0, 0, 267, 268, 1,
|
||||
0, 0, 0, 268, 266, 1, 0, 0, 0, 268, 269, 1, 0, 0, 0, 269, 271, 1, 0, 0,
|
||||
0, 270, 261, 1, 0, 0, 0, 270, 271, 1, 0, 0, 0, 271, 273, 1, 0, 0, 0, 272,
|
||||
225, 1, 0, 0, 0, 272, 253, 1, 0, 0, 0, 273, 60, 1, 0, 0, 0, 274, 280, 5,
|
||||
34, 0, 0, 275, 279, 8, 23, 0, 0, 276, 277, 5, 92, 0, 0, 277, 279, 9, 0,
|
||||
0, 0, 278, 275, 1, 0, 0, 0, 278, 276, 1, 0, 0, 0, 279, 282, 1, 0, 0, 0,
|
||||
280, 278, 1, 0, 0, 0, 280, 281, 1, 0, 0, 0, 281, 283, 1, 0, 0, 0, 282,
|
||||
280, 1, 0, 0, 0, 283, 295, 5, 34, 0, 0, 284, 290, 5, 39, 0, 0, 285, 289,
|
||||
8, 24, 0, 0, 286, 287, 5, 92, 0, 0, 287, 289, 9, 0, 0, 0, 288, 285, 1,
|
||||
0, 0, 0, 288, 286, 1, 0, 0, 0, 289, 292, 1, 0, 0, 0, 290, 288, 1, 0, 0,
|
||||
0, 290, 291, 1, 0, 0, 0, 291, 293, 1, 0, 0, 0, 292, 290, 1, 0, 0, 0, 293,
|
||||
295, 5, 39, 0, 0, 294, 274, 1, 0, 0, 0, 294, 284, 1, 0, 0, 0, 295, 62,
|
||||
1, 0, 0, 0, 296, 300, 7, 25, 0, 0, 297, 299, 7, 26, 0, 0, 298, 297, 1,
|
||||
0, 0, 0, 299, 302, 1, 0, 0, 0, 300, 298, 1, 0, 0, 0, 300, 301, 1, 0, 0,
|
||||
0, 301, 64, 1, 0, 0, 0, 302, 300, 1, 0, 0, 0, 303, 304, 5, 91, 0, 0, 304,
|
||||
305, 5, 93, 0, 0, 305, 66, 1, 0, 0, 0, 306, 307, 5, 91, 0, 0, 307, 308,
|
||||
5, 42, 0, 0, 308, 309, 5, 93, 0, 0, 309, 68, 1, 0, 0, 0, 310, 317, 3, 63,
|
||||
31, 0, 311, 312, 5, 46, 0, 0, 312, 316, 3, 63, 31, 0, 313, 316, 3, 65,
|
||||
32, 0, 314, 316, 3, 67, 33, 0, 315, 311, 1, 0, 0, 0, 315, 313, 1, 0, 0,
|
||||
0, 315, 314, 1, 0, 0, 0, 316, 319, 1, 0, 0, 0, 317, 315, 1, 0, 0, 0, 317,
|
||||
318, 1, 0, 0, 0, 318, 70, 1, 0, 0, 0, 319, 317, 1, 0, 0, 0, 320, 322, 7,
|
||||
27, 0, 0, 321, 320, 1, 0, 0, 0, 322, 323, 1, 0, 0, 0, 323, 321, 1, 0, 0,
|
||||
0, 323, 324, 1, 0, 0, 0, 324, 325, 1, 0, 0, 0, 325, 326, 6, 35, 0, 0, 326,
|
||||
72, 1, 0, 0, 0, 327, 328, 7, 28, 0, 0, 328, 74, 1, 0, 0, 0, 329, 331, 8,
|
||||
29, 0, 0, 330, 329, 1, 0, 0, 0, 331, 332, 1, 0, 0, 0, 332, 330, 1, 0, 0,
|
||||
0, 332, 333, 1, 0, 0, 0, 333, 76, 1, 0, 0, 0, 30, 0, 90, 119, 138, 160,
|
||||
177, 220, 225, 230, 236, 239, 243, 248, 250, 253, 259, 263, 268, 270, 272,
|
||||
278, 280, 288, 290, 294, 300, 315, 317, 323, 332, 1, 6, 0, 0,
|
||||
}
|
||||
deserializer := antlr.NewATNDeserializer(nil)
|
||||
staticData.atn = deserializer.Deserialize(staticData.serializedATN)
|
||||
@@ -325,12 +290,9 @@ const (
|
||||
FilterQueryLexerHASANY = 26
|
||||
FilterQueryLexerHASALL = 27
|
||||
FilterQueryLexerBOOL = 28
|
||||
FilterQueryLexerDOLLAR_VAR = 29
|
||||
FilterQueryLexerCURLY_VAR = 30
|
||||
FilterQueryLexerSQUARE_VAR = 31
|
||||
FilterQueryLexerNUMBER = 32
|
||||
FilterQueryLexerQUOTED_TEXT = 33
|
||||
FilterQueryLexerKEY = 34
|
||||
FilterQueryLexerWS = 35
|
||||
FilterQueryLexerFREETEXT = 36
|
||||
FilterQueryLexerNUMBER = 29
|
||||
FilterQueryLexerQUOTED_TEXT = 30
|
||||
FilterQueryLexerKEY = 31
|
||||
FilterQueryLexerWS = 32
|
||||
FilterQueryLexerFREETEXT = 33
|
||||
)
|
||||
|
||||
@@ -56,9 +56,6 @@ type FilterQueryListener interface {
|
||||
// EnterValue is called when entering the value production.
|
||||
EnterValue(c *ValueContext)
|
||||
|
||||
// EnterVariable is called when entering the variable production.
|
||||
EnterVariable(c *VariableContext)
|
||||
|
||||
// EnterKey is called when entering the key production.
|
||||
EnterKey(c *KeyContext)
|
||||
|
||||
@@ -110,9 +107,6 @@ type FilterQueryListener interface {
|
||||
// ExitValue is called when exiting the value production.
|
||||
ExitValue(c *ValueContext)
|
||||
|
||||
// ExitVariable is called when exiting the variable production.
|
||||
ExitVariable(c *VariableContext)
|
||||
|
||||
// ExitKey is called when exiting the key production.
|
||||
ExitKey(c *KeyContext)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,9 +56,6 @@ type FilterQueryVisitor interface {
|
||||
// Visit a parse tree produced by FilterQueryParser#value.
|
||||
VisitValue(ctx *ValueContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by FilterQueryParser#variable.
|
||||
VisitVariable(ctx *VariableContext) interface{}
|
||||
|
||||
// Visit a parse tree produced by FilterQueryParser#key.
|
||||
VisitKey(ctx *KeyContext) interface{}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ type builderQuery[T any] struct {
|
||||
telemetryStore telemetrystore.TelemetryStore
|
||||
stmtBuilder qbtypes.StatementBuilder[T]
|
||||
spec qbtypes.QueryBuilderQuery[T]
|
||||
variables map[string]qbtypes.VariableItem
|
||||
|
||||
fromMS uint64
|
||||
toMS uint64
|
||||
@@ -33,13 +32,11 @@ func newBuilderQuery[T any](
|
||||
spec qbtypes.QueryBuilderQuery[T],
|
||||
tr qbtypes.TimeRange,
|
||||
kind qbtypes.RequestType,
|
||||
variables map[string]qbtypes.VariableItem,
|
||||
) *builderQuery[T] {
|
||||
return &builderQuery[T]{
|
||||
telemetryStore: telemetryStore,
|
||||
stmtBuilder: stmtBuilder,
|
||||
spec: spec,
|
||||
variables: variables,
|
||||
fromMS: tr.From,
|
||||
toMS: tr.To,
|
||||
kind: kind,
|
||||
@@ -177,7 +174,7 @@ func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error)
|
||||
return q.executeWindowList(ctx)
|
||||
}
|
||||
|
||||
stmt, err := q.stmtBuilder.Build(ctx, q.fromMS, q.toMS, q.kind, q.spec, q.variables)
|
||||
stmt, err := q.stmtBuilder.Build(ctx, q.fromMS, q.toMS, q.kind, q.spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -281,7 +278,7 @@ func (q *builderQuery[T]) executeWindowList(ctx context.Context) (*qbtypes.Resul
|
||||
q.spec.Offset = 0
|
||||
q.spec.Limit = need
|
||||
|
||||
stmt, err := q.stmtBuilder.Build(ctx, r.fromNS/1e6, r.toNS/1e6, q.kind, q.spec, q.variables)
|
||||
stmt, err := q.stmtBuilder.Build(ctx, r.fromNS/1e6, r.toNS/1e6, q.kind, q.spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,11 +3,8 @@ package querier
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/govaluate"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
@@ -109,15 +106,12 @@ func postProcessBuilderQuery[T any](
|
||||
q *querier,
|
||||
result *qbtypes.Result,
|
||||
query qbtypes.QueryBuilderQuery[T],
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
_ *qbtypes.QueryRangeRequest,
|
||||
) *qbtypes.Result {
|
||||
|
||||
// Apply functions
|
||||
if len(query.Functions) > 0 {
|
||||
// For builder queries, use the query's own step
|
||||
step := query.StepInterval.Duration.Milliseconds()
|
||||
functions := q.prepareFillZeroArgsWithStep(query.Functions, req, step)
|
||||
result = q.applyFunctions(result, functions)
|
||||
result = q.applyFunctions(result, query.Functions)
|
||||
}
|
||||
|
||||
return result
|
||||
@@ -136,10 +130,7 @@ func postProcessMetricQuery(
|
||||
}
|
||||
|
||||
if len(query.Functions) > 0 {
|
||||
// For metric queries, use the query's own step
|
||||
step := query.StepInterval.Duration.Milliseconds()
|
||||
functions := q.prepareFillZeroArgsWithStep(query.Functions, req, step)
|
||||
result = q.applyFunctions(result, functions)
|
||||
result = q.applyFunctions(result, query.Functions)
|
||||
}
|
||||
|
||||
// Apply reduce to for scalar request type
|
||||
@@ -231,11 +222,6 @@ func (q *querier) applyFormulas(ctx context.Context, results map[string]*qbtypes
|
||||
if result != nil {
|
||||
results[name] = result
|
||||
}
|
||||
} else if req.RequestType == qbtypes.RequestTypeScalar {
|
||||
result := q.processScalarFormula(ctx, results, formula, req)
|
||||
if result != nil {
|
||||
results[name] = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +233,7 @@ func (q *querier) processTimeSeriesFormula(
|
||||
ctx context.Context,
|
||||
results map[string]*qbtypes.Result,
|
||||
formula qbtypes.QueryBuilderFormula,
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
_ *qbtypes.QueryRangeRequest,
|
||||
) *qbtypes.Result {
|
||||
// Prepare time series data for formula evaluation
|
||||
timeSeriesData := make(map[string]*qbtypes.TimeSeriesData)
|
||||
@@ -292,218 +278,12 @@ func (q *querier) processTimeSeriesFormula(
|
||||
}
|
||||
|
||||
if len(formula.Functions) > 0 {
|
||||
// For formulas, calculate GCD of steps from queries in the expression
|
||||
step := q.calculateFormulaStep(formula.Expression, req)
|
||||
functions := q.prepareFillZeroArgsWithStep(formula.Functions, req, step)
|
||||
result = q.applyFunctions(result, functions)
|
||||
result = q.applyFunctions(result, formula.Functions)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// processScalarFormula handles formula evaluation for scalar data
|
||||
//
|
||||
// NOTE: This implementation has a known limitation with formulas that reference
|
||||
// specific aggregations by index (e.g., "A.0", "A.1") or multiple aggregations
|
||||
// from the same query (e.g., "A.0 * 2 + A.1"). The FormulaEvaluator's series
|
||||
// matching logic doesn't work correctly when converting scalar data to time series
|
||||
// format for these cases.
|
||||
//
|
||||
// Currently supported:
|
||||
// - Formulas between different queries: "A / B", "A * 2 + B"
|
||||
// - Simple references: "A" (defaults to first aggregation)
|
||||
//
|
||||
// Not supported:
|
||||
// - Indexed aggregation references: "A.0", "A.1"
|
||||
// - Multiple aggregations from same query: "A.0 + A.1"
|
||||
//
|
||||
// To properly support this, we would need to either:
|
||||
// 1. Fix the FormulaEvaluator's series lookup logic for scalar-converted data
|
||||
// 2. Implement a dedicated scalar formula evaluator
|
||||
func (q *querier) processScalarFormula(
|
||||
ctx context.Context,
|
||||
results map[string]*qbtypes.Result,
|
||||
formula qbtypes.QueryBuilderFormula,
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
) *qbtypes.Result {
|
||||
// Convert scalar data to time series format with zero timestamp
|
||||
timeSeriesData := make(map[string]*qbtypes.TimeSeriesData)
|
||||
|
||||
for queryName, result := range results {
|
||||
if scalarData, ok := result.Value.(*qbtypes.ScalarData); ok {
|
||||
// Convert scalar to time series
|
||||
tsData := &qbtypes.TimeSeriesData{
|
||||
QueryName: scalarData.QueryName,
|
||||
Aggregations: make([]*qbtypes.AggregationBucket, 0),
|
||||
}
|
||||
|
||||
// Find aggregation columns
|
||||
aggColumns := make(map[int]int) // aggregation index -> column index
|
||||
for colIdx, col := range scalarData.Columns {
|
||||
if col.Type == qbtypes.ColumnTypeAggregation {
|
||||
aggColumns[int(col.AggregationIndex)] = colIdx
|
||||
}
|
||||
}
|
||||
|
||||
// Group rows by their label sets
|
||||
type labeledRowData struct {
|
||||
labels []*qbtypes.Label
|
||||
values map[int]float64 // aggregation index -> value
|
||||
}
|
||||
|
||||
// First pass: group all rows by their label combination
|
||||
rowsByLabels := make(map[string]*labeledRowData)
|
||||
for _, row := range scalarData.Data {
|
||||
// Build labels from group columns
|
||||
labels := make([]*qbtypes.Label, 0)
|
||||
for i, col := range scalarData.Columns {
|
||||
if col.Type == qbtypes.ColumnTypeGroup && i < len(row) {
|
||||
labels = append(labels, &qbtypes.Label{
|
||||
Key: col.TelemetryFieldKey,
|
||||
Value: row[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
labelKey := qbtypes.GetUniqueSeriesKey(labels)
|
||||
|
||||
// Get or create row data
|
||||
rowData, exists := rowsByLabels[labelKey]
|
||||
if !exists {
|
||||
rowData = &labeledRowData{
|
||||
labels: labels,
|
||||
values: make(map[int]float64),
|
||||
}
|
||||
rowsByLabels[labelKey] = rowData
|
||||
}
|
||||
|
||||
// Store all aggregation values from this row
|
||||
for aggIdx, colIdx := range aggColumns {
|
||||
if colIdx < len(row) {
|
||||
if val, ok := toFloat64(row[colIdx]); ok {
|
||||
rowData.values[aggIdx] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get sorted label keys for consistent ordering
|
||||
labelKeys := make([]string, 0, len(rowsByLabels))
|
||||
for key := range rowsByLabels {
|
||||
labelKeys = append(labelKeys, key)
|
||||
}
|
||||
slices.Sort(labelKeys)
|
||||
|
||||
// Create aggregation buckets
|
||||
aggIndices := make([]int, 0, len(aggColumns))
|
||||
for aggIdx := range aggColumns {
|
||||
aggIndices = append(aggIndices, aggIdx)
|
||||
}
|
||||
slices.Sort(aggIndices)
|
||||
|
||||
// For each aggregation, create a bucket with series in consistent order
|
||||
for _, aggIdx := range aggIndices {
|
||||
colIdx := aggColumns[aggIdx]
|
||||
|
||||
bucket := &qbtypes.AggregationBucket{
|
||||
Index: aggIdx,
|
||||
Alias: scalarData.Columns[colIdx].Name,
|
||||
Meta: scalarData.Columns[colIdx].Meta,
|
||||
Series: make([]*qbtypes.TimeSeries, 0),
|
||||
}
|
||||
|
||||
// Create series in the same order (by label key)
|
||||
for _, labelKey := range labelKeys {
|
||||
rowData := rowsByLabels[labelKey]
|
||||
|
||||
// Only create series if we have a value for this aggregation
|
||||
if val, exists := rowData.values[aggIdx]; exists {
|
||||
series := &qbtypes.TimeSeries{
|
||||
Labels: rowData.labels,
|
||||
Values: []*qbtypes.TimeSeriesValue{{
|
||||
Timestamp: 0,
|
||||
Value: val,
|
||||
}},
|
||||
}
|
||||
bucket.Series = append(bucket.Series, series)
|
||||
}
|
||||
}
|
||||
|
||||
tsData.Aggregations = append(tsData.Aggregations, bucket)
|
||||
}
|
||||
|
||||
timeSeriesData[queryName] = tsData
|
||||
}
|
||||
}
|
||||
|
||||
// Create formula evaluator
|
||||
canDefaultZero := make(map[string]bool)
|
||||
evaluator, err := qbtypes.NewFormulaEvaluator(formula.Expression, canDefaultZero)
|
||||
if err != nil {
|
||||
q.logger.ErrorContext(ctx, "failed to create formula evaluator", "error", err, "formula", formula.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Evaluate the formula
|
||||
formulaSeries, err := evaluator.EvaluateFormula(timeSeriesData)
|
||||
if err != nil {
|
||||
q.logger.ErrorContext(ctx, "failed to evaluate formula", "error", err, "formula", formula.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert back to scalar format
|
||||
scalarResult := &qbtypes.ScalarData{
|
||||
QueryName: formula.Name,
|
||||
Columns: make([]*qbtypes.ColumnDescriptor, 0),
|
||||
Data: make([][]any, 0),
|
||||
}
|
||||
|
||||
// Build columns from first series
|
||||
if len(formulaSeries) > 0 && len(formulaSeries[0].Labels) > 0 {
|
||||
// Add group columns
|
||||
for _, label := range formulaSeries[0].Labels {
|
||||
scalarResult.Columns = append(scalarResult.Columns, &qbtypes.ColumnDescriptor{
|
||||
TelemetryFieldKey: label.Key,
|
||||
QueryName: formula.Name,
|
||||
Type: qbtypes.ColumnTypeGroup,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add result column
|
||||
scalarResult.Columns = append(scalarResult.Columns, &qbtypes.ColumnDescriptor{
|
||||
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "__result"},
|
||||
QueryName: formula.Name,
|
||||
AggregationIndex: 0,
|
||||
Type: qbtypes.ColumnTypeAggregation,
|
||||
})
|
||||
|
||||
// Build rows
|
||||
for _, series := range formulaSeries {
|
||||
row := make([]any, len(scalarResult.Columns))
|
||||
|
||||
// Add group values
|
||||
for i, label := range series.Labels {
|
||||
if i < len(row)-1 {
|
||||
row[i] = label.Value
|
||||
}
|
||||
}
|
||||
|
||||
// Add aggregation value (from single value at timestamp 0)
|
||||
if len(series.Values) > 0 {
|
||||
row[len(row)-1] = series.Values[0].Value
|
||||
} else {
|
||||
row[len(row)-1] = "n/a"
|
||||
}
|
||||
|
||||
scalarResult.Data = append(scalarResult.Data, row)
|
||||
}
|
||||
|
||||
return &qbtypes.Result{
|
||||
Value: scalarResult,
|
||||
}
|
||||
}
|
||||
|
||||
// filterDisabledQueries removes results for disabled queries
|
||||
func (q *querier) filterDisabledQueries(results map[string]*qbtypes.Result, req *qbtypes.QueryRangeRequest) map[string]*qbtypes.Result {
|
||||
filtered := make(map[string]*qbtypes.Result)
|
||||
@@ -870,98 +650,3 @@ func toFloat64(v any) (float64, bool) {
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// gcd calculates the greatest common divisor
|
||||
func gcd(a, b int64) int64 {
|
||||
if b == 0 {
|
||||
return a
|
||||
}
|
||||
return gcd(b, a%b)
|
||||
}
|
||||
|
||||
// prepareFillZeroArgsWithStep prepares fillZero function arguments with a specific step
|
||||
func (q *querier) prepareFillZeroArgsWithStep(functions []qbtypes.Function, req *qbtypes.QueryRangeRequest, step int64) []qbtypes.Function {
|
||||
// Check if we need to modify any functions
|
||||
needsCopy := false
|
||||
for _, fn := range functions {
|
||||
if fn.Name == qbtypes.FunctionNameFillZero && len(fn.Args) == 0 {
|
||||
needsCopy = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no fillZero functions need arguments, return original slice
|
||||
if !needsCopy {
|
||||
return functions
|
||||
}
|
||||
|
||||
// Only copy if we need to modify
|
||||
updatedFunctions := make([]qbtypes.Function, len(functions))
|
||||
copy(updatedFunctions, functions)
|
||||
|
||||
// Process each function
|
||||
for i, fn := range updatedFunctions {
|
||||
if fn.Name == qbtypes.FunctionNameFillZero && len(fn.Args) == 0 {
|
||||
// Set the arguments: start, end, step
|
||||
fn.Args = []qbtypes.FunctionArg{
|
||||
{Value: float64(req.Start)},
|
||||
{Value: float64(req.End)},
|
||||
{Value: float64(step)},
|
||||
}
|
||||
updatedFunctions[i] = fn
|
||||
}
|
||||
}
|
||||
|
||||
return updatedFunctions
|
||||
}
|
||||
|
||||
// calculateFormulaStep calculates the GCD of steps from queries referenced in the formula
|
||||
func (q *querier) calculateFormulaStep(expression string, req *qbtypes.QueryRangeRequest) int64 {
|
||||
// Use govaluate to parse the expression and extract variables
|
||||
// This is the same library used by FormulaEvaluator
|
||||
parsedExpr, err := govaluate.NewEvaluableExpression(expression)
|
||||
if err != nil {
|
||||
// If we can't parse the expression, use default
|
||||
return 60000
|
||||
}
|
||||
|
||||
// Get the variables from the parsed expression
|
||||
variables := parsedExpr.Vars()
|
||||
|
||||
// Extract base query names (e.g., "A" from "A.0" or "A.my_alias")
|
||||
queryNames := make(map[string]bool)
|
||||
for _, variable := range variables {
|
||||
// Split by "." to get the base query name
|
||||
parts := strings.Split(variable, ".")
|
||||
if len(parts) > 0 {
|
||||
queryNames[parts[0]] = true
|
||||
}
|
||||
}
|
||||
|
||||
var steps []int64
|
||||
|
||||
// Collect steps only from queries referenced in the formula
|
||||
for _, query := range req.CompositeQuery.Queries {
|
||||
info := getqueryInfo(query.Spec)
|
||||
// Check if this query is referenced in the formula
|
||||
if !info.Disabled && queryNames[info.Name] && info.Step.Duration > 0 {
|
||||
stepMs := info.Step.Duration.Milliseconds()
|
||||
if stepMs > 0 {
|
||||
steps = append(steps, stepMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no steps found, use a default (60 seconds)
|
||||
if len(steps) == 0 {
|
||||
return 60000
|
||||
}
|
||||
|
||||
// Calculate GCD of all steps
|
||||
result := steps[0]
|
||||
for i := 1; i < len(steps); i++ {
|
||||
result = gcd(result, steps[i])
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user