Merge pull request #5618 from SigNoz/release/v0.51.x

Release/v0.51.x
This commit is contained in:
Prashant Shahi 2024-07-31 22:30:09 +05:30 committed by GitHub
commit a476c68f7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
112 changed files with 4884 additions and 628 deletions

View File

@ -30,6 +30,7 @@ jobs:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }} GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }} GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }} GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: | run: |
read -r -d '' COMMAND <<EOF || true read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}" echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@ -51,4 +52,4 @@ jobs:
make build-frontend-amd64 make build-frontend-amd64
make run-testing make run-testing
EOF EOF
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}" gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@ -30,6 +30,7 @@ jobs:
GCP_PROJECT: ${{ secrets.GCP_PROJECT }} GCP_PROJECT: ${{ secrets.GCP_PROJECT }}
GCP_ZONE: ${{ secrets.GCP_ZONE }} GCP_ZONE: ${{ secrets.GCP_ZONE }}
GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }} GCP_INSTANCE: ${{ secrets.GCP_INSTANCE }}
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
run: | run: |
read -r -d '' COMMAND <<EOF || true read -r -d '' COMMAND <<EOF || true
echo "GITHUB_BRANCH: ${GITHUB_BRANCH}" echo "GITHUB_BRANCH: ${GITHUB_BRANCH}"
@ -52,4 +53,4 @@ jobs:
make build-frontend-amd64 make build-frontend-amd64
make run-testing make run-testing
EOF EOF
gcloud compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}" gcloud beta compute ssh ${GCP_INSTANCE} --zone ${GCP_ZONE} --ssh-key-expire-after=15m --tunnel-through-iap --project ${GCP_PROJECT} --command "${COMMAND}"

View File

@ -146,7 +146,7 @@ services:
condition: on-failure condition: on-failure
query-service: query-service:
image: signoz/query-service:0.50.0 image: signoz/query-service:0.51.0
command: command:
[ [
"-config=/root/config/prometheus.yml", "-config=/root/config/prometheus.yml",
@ -199,7 +199,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:0.102.2 image: signoz/signoz-otel-collector:0.102.3
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",
@ -237,7 +237,7 @@ services:
- query-service - query-service
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:0.102.2 image: signoz/signoz-schema-migrator:0.102.3
deploy: deploy:
restart_policy: restart_policy:
condition: on-failure condition: on-failure

View File

@ -66,7 +66,7 @@ services:
- --storage.path=/data - --storage.path=/data
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -81,7 +81,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
otel-collector: otel-collector:
container_name: signoz-otel-collector container_name: signoz-otel-collector
image: signoz/signoz-otel-collector:0.102.2 image: signoz/signoz-otel-collector:0.102.3
command: command:
[ [
"--config=/etc/otel-collector-config.yaml", "--config=/etc/otel-collector-config.yaml",

View File

@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.50.0} image: signoz/query-service:${DOCKER_TAG:-0.51.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -204,7 +204,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.50.0} image: signoz/frontend:${DOCKER_TAG:-0.51.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -216,7 +216,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -230,7 +230,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.3}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -164,7 +164,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md` # Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service: query-service:
image: signoz/query-service:${DOCKER_TAG:-0.50.0} image: signoz/query-service:${DOCKER_TAG:-0.51.0}
container_name: signoz-query-service container_name: signoz-query-service
command: command:
[ [
@ -203,7 +203,7 @@ services:
<<: *db-depend <<: *db-depend
frontend: frontend:
image: signoz/frontend:${DOCKER_TAG:-0.50.0} image: signoz/frontend:${DOCKER_TAG:-0.51.0}
container_name: signoz-frontend container_name: signoz-frontend
restart: on-failure restart: on-failure
depends_on: depends_on:
@ -215,7 +215,7 @@ services:
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf - ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
otel-collector-migrator: otel-collector-migrator:
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.2} image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.102.3}
container_name: otel-migrator container_name: otel-migrator
command: command:
- "--dsn=tcp://clickhouse:9000" - "--dsn=tcp://clickhouse:9000"
@ -229,7 +229,7 @@ services:
otel-collector: otel-collector:
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.2} image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.102.3}
container_name: signoz-otel-collector container_name: signoz-otel-collector
command: command:
[ [

View File

@ -1,7 +1,9 @@
package api package api
import ( import (
"errors"
"net/http" "net/http"
"strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"go.signoz.io/signoz/pkg/query-service/app/dashboards" "go.signoz.io/signoz/pkg/query-service/app/dashboards"
@ -29,6 +31,10 @@ func (ah *APIHandler) lockUnlockDashboard(w http.ResponseWriter, r *http.Request
// Get the dashboard UUID from the request // Get the dashboard UUID from the request
uuid := mux.Vars(r)["uuid"] uuid := mux.Vars(r)["uuid"]
if strings.HasPrefix(uuid,"integration") {
RespondError(w, &model.ApiError{Typ: model.ErrorForbidden, Err: errors.New("dashboards created by integrations cannot be unlocked")}, "You are not authorized to lock/unlock this dashboard")
return
}
dashboard, err := dashboards.GetDashboard(r.Context(), uuid) dashboard, err := dashboards.GetDashboard(r.Context(), uuid)
if err != nil { if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error()) RespondError(w, &model.ApiError{Typ: model.ErrorInternal, Err: err}, err.Error())

View File

@ -4,11 +4,11 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
"net/http/httputil"
_ "net/http/pprof" // http profiler _ "net/http/pprof" // http profiler
"os" "os"
"regexp" "regexp"
@ -28,6 +28,7 @@ import (
"go.signoz.io/signoz/ee/query-service/integrations/gateway" "go.signoz.io/signoz/ee/query-service/integrations/gateway"
"go.signoz.io/signoz/ee/query-service/interfaces" "go.signoz.io/signoz/ee/query-service/interfaces"
baseauth "go.signoz.io/signoz/pkg/query-service/auth" baseauth "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/model"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
licensepkg "go.signoz.io/signoz/ee/query-service/license" licensepkg "go.signoz.io/signoz/ee/query-service/license"
@ -41,6 +42,7 @@ import (
"go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline" "go.signoz.io/signoz/pkg/query-service/app/logparsingpipeline"
"go.signoz.io/signoz/pkg/query-service/app/opamp" "go.signoz.io/signoz/pkg/query-service/app/opamp"
opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model" opAmpModel "go.signoz.io/signoz/pkg/query-service/app/opamp/model"
"go.signoz.io/signoz/pkg/query-service/app/preferences"
"go.signoz.io/signoz/pkg/query-service/cache" "go.signoz.io/signoz/pkg/query-service/cache"
baseconst "go.signoz.io/signoz/pkg/query-service/constants" baseconst "go.signoz.io/signoz/pkg/query-service/constants"
"go.signoz.io/signoz/pkg/query-service/healthcheck" "go.signoz.io/signoz/pkg/query-service/healthcheck"
@ -110,6 +112,10 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH) baseexplorer.InitWithDSN(baseconst.RELATIONAL_DATASOURCE_PATH)
if err := preferences.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH); err != nil {
return nil, err
}
localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH) localDB, err := dashboards.InitDB(baseconst.RELATIONAL_DATASOURCE_PATH)
if err != nil { if err != nil {
@ -118,33 +124,13 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
localDB.SetMaxOpenConns(10) localDB.SetMaxOpenConns(10)
gatewayFeature := basemodel.Feature{ gatewayProxy, err := gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
Name: "GATEWAY", if err != nil {
Active: false, return nil, err
Usage: 0,
UsageLimit: -1,
Route: "",
}
//Activate this feature if the url is not empty
var gatewayProxy *httputil.ReverseProxy
if serverOptions.GatewayUrl == "" {
gatewayFeature.Active = false
gatewayProxy, err = gateway.NewNoopProxy()
if err != nil {
return nil, err
}
} else {
zap.L().Info("Enabling gateway feature flag ...")
gatewayFeature.Active = true
gatewayProxy, err = gateway.NewProxy(serverOptions.GatewayUrl, gateway.RoutePrefix)
if err != nil {
return nil, err
}
} }
// initiate license manager // initiate license manager
lm, err := licensepkg.StartManager("sqlite", localDB, gatewayFeature) lm, err := licensepkg.StartManager("sqlite", localDB)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -340,7 +326,17 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler) (*http.Server, e
// add auth middleware // add auth middleware
getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) { getUserFromRequest := func(r *http.Request) (*basemodel.UserPayload, error) {
return auth.GetUserFromRequest(r, apiHandler) user, err := auth.GetUserFromRequest(r, apiHandler)
if err != nil {
return nil, err
}
if user.User.OrgId == "" {
return nil, model.UnauthorizedError(errors.New("orgId is missing in the claims"))
}
return user, nil
} }
am := baseapp.NewAuthMiddleware(getUserFromRequest) am := baseapp.NewAuthMiddleware(getUserFromRequest)

View File

@ -20,11 +20,14 @@ import (
func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) { func (m *modelDao) createUserForSAMLRequest(ctx context.Context, email string) (*basemodel.User, basemodel.BaseApiError) {
// get auth domain from email domain // get auth domain from email domain
domain, apierr := m.GetDomainByEmail(ctx, email) domain, apierr := m.GetDomainByEmail(ctx, email)
if apierr != nil { if apierr != nil {
zap.L().Error("failed to get domain from email", zap.Error(apierr)) zap.L().Error("failed to get domain from email", zap.Error(apierr))
return nil, model.InternalErrorStr("failed to get domain from email") return nil, model.InternalErrorStr("failed to get domain from email")
} }
if domain == nil {
zap.L().Error("email domain does not match any authenticated domain", zap.String("email", email))
return nil, model.InternalErrorStr("email domain does not match any authenticated domain")
}
hash, err := baseauth.PasswordHash(utils.GeneratePassowrd()) hash, err := baseauth.PasswordHash(utils.GeneratePassowrd())
if err != nil { if err != nil {

View File

@ -5,5 +5,5 @@ import (
) )
func NewNoopProxy() (*httputil.ReverseProxy, error) { func NewNoopProxy() (*httputil.ReverseProxy, error) {
return nil, nil return &httputil.ReverseProxy{}, nil
} }

View File

@ -11,6 +11,7 @@ const Enterprise = "ENTERPRISE_PLAN"
const DisableUpsell = "DISABLE_UPSELL" const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING" const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT" const ChatSupport = "CHAT_SUPPORT"
const Gateway = "GATEWAY"
var BasicPlan = basemodel.FeatureSet{ var BasicPlan = basemodel.FeatureSet{
basemodel.Feature{ basemodel.Feature{
@ -111,6 +112,13 @@ var BasicPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: Gateway,
Active: false,
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }
var ProPlan = basemodel.FeatureSet{ var ProPlan = basemodel.FeatureSet{
@ -205,6 +213,13 @@ var ProPlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }
var EnterprisePlan = basemodel.FeatureSet{ var EnterprisePlan = basemodel.FeatureSet{
@ -313,4 +328,11 @@ var EnterprisePlan = basemodel.FeatureSet{
UsageLimit: -1, UsageLimit: -1,
Route: "", Route: "",
}, },
basemodel.Feature{
Name: Gateway,
Active: true,
Usage: 0,
UsageLimit: -1,
Route: "",
},
} }

View File

@ -1,6 +1,7 @@
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import getLocalStorageApi from 'api/browser/localstorage/get'; import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set'; import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import NotFound from 'components/NotFound'; import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
@ -48,7 +49,7 @@ function App(): JSX.Element {
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
const { trackPageView, trackEvent } = useAnalytics(); const { trackPageView } = useAnalytics();
const { hostname, pathname } = window.location; const { hostname, pathname } = window.location;
@ -199,7 +200,7 @@ function App(): JSX.Element {
LOCALSTORAGE.THEME_ANALYTICS_V1, LOCALSTORAGE.THEME_ANALYTICS_V1,
); );
if (!isThemeAnalyticsSent) { if (!isThemeAnalyticsSent) {
trackEvent('Theme Analytics', { logEvent('Theme Analytics', {
theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT, theme: isDarkMode ? THEME_MODE.DARK : THEME_MODE.LIGHT,
user: pick(user, ['email', 'userId', 'name']), user: pick(user, ['email', 'userId', 'name']),
org, org,

View File

@ -1,4 +1,4 @@
import axios from 'api'; import { ApiBaseInstance as axios } from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api'; import { ErrorResponse, SuccessResponse } from 'types/api';
@ -21,6 +21,7 @@ const logEvent = async (
payload: response.data.data, payload: response.data.data,
}; };
} catch (error) { } catch (error) {
console.error(error);
return ErrorResponseHandler(error as AxiosError); return ErrorResponseHandler(error as AxiosError);
} }
}; };

View File

@ -96,6 +96,10 @@ const interceptorRejected = async (
} }
}; };
const interceptorRejectedBase = async (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => Promise.reject(value);
const instance = axios.create({ const instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`, baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
}); });
@ -140,6 +144,18 @@ ApiV4Instance.interceptors.response.use(
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse); ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
// //
// axios Base
export const ApiBaseInstance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,
});
ApiBaseInstance.interceptors.response.use(
interceptorsResponse,
interceptorRejectedBase,
);
ApiBaseInstance.interceptors.request.use(interceptorsRequestResponse);
//
// gateway Api V1 // gateway Api V1
export const GatewayApiV1Instance = axios.create({ export const GatewayApiV1Instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`, baseURL: `${ENVIRONMENT.baseURL}${gatewayApiV1}`,

View File

@ -5,7 +5,6 @@ import { Button } from 'antd';
import { Tag } from 'antd/lib'; import { Tag } from 'antd/lib';
import Input from 'components/Input'; import Input from 'components/Input';
import { Check, X } from 'lucide-react'; import { Check, X } from 'lucide-react';
import { TweenOneGroup } from 'rc-tween-one';
import React, { Dispatch, SetStateAction, useState } from 'react'; import React, { Dispatch, SetStateAction, useState } from 'react';
function Tags({ tags, setTags }: AddTagsProps): JSX.Element { function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
@ -46,41 +45,19 @@ function Tags({ tags, setTags }: AddTagsProps): JSX.Element {
func(value); func(value);
}; };
const forMap = (tag: string): React.ReactElement => (
<span key={tag} style={{ display: 'inline-block' }}>
<Tag
closable
onClose={(e): void => {
e.preventDefault();
handleClose(tag);
}}
>
{tag}
</Tag>
</span>
);
const tagChild = tags.map(forMap);
const renderTagsAnimated = (): React.ReactElement => (
<TweenOneGroup
appear={false}
className="tags"
enter={{ scale: 0.8, opacity: 0, type: 'from', duration: 100 }}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
onEnd={(e): void => {
if (e.type === 'appear' || e.type === 'enter') {
(e.target as any).style = 'display: inline-block';
}
}}
>
{tagChild}
</TweenOneGroup>
);
return ( return (
<div className="tags-container"> <div className="tags-container">
{renderTagsAnimated()} {tags.map<React.ReactNode>((tag) => (
<Tag
key={tag}
closable
style={{ userSelect: 'none' }}
onClose={(): void => handleClose(tag)}
>
<span>{tag}</span>
</Tag>
))}
{inputVisible && ( {inputVisible && (
<div className="add-tag-container"> <div className="add-tag-container">
<Input <Input

View File

@ -49,7 +49,10 @@ function ValueGraph({
} }
> >
<Tooltip title={t('this_value_satisfies_multiple_thresholds')}> <Tooltip title={t('this_value_satisfies_multiple_thresholds')}>
<ExclamationCircleFilled className="value-graph-icon" /> <ExclamationCircleFilled
className="value-graph-icon"
data-testid="conflicting-thresholds"
/>
</Tooltip> </Tooltip>
</div> </div>
)} )}

View File

@ -16,6 +16,7 @@ export interface FacingIssueBtnProps {
buttonText?: string; buttonText?: string;
className?: string; className?: string;
onHoverText?: string; onHoverText?: string;
intercomMessageDisabled?: boolean;
} }
function FacingIssueBtn({ function FacingIssueBtn({
@ -25,11 +26,12 @@ function FacingIssueBtn({
buttonText = '', buttonText = '',
className = '', className = '',
onHoverText = '', onHoverText = '',
intercomMessageDisabled = false,
}: FacingIssueBtnProps): JSX.Element | null { }: FacingIssueBtnProps): JSX.Element | null {
const handleFacingIssuesClick = (): void => { const handleFacingIssuesClick = (): void => {
logEvent(eventName, attributes); logEvent(eventName, attributes);
if (window.Intercom) { if (window.Intercom && !intercomMessageDisabled) {
window.Intercom('showNewMessage', defaultTo(message, '')); window.Intercom('showNewMessage', defaultTo(message, ''));
} }
}; };
@ -62,6 +64,7 @@ FacingIssueBtn.defaultProps = {
buttonText: '', buttonText: '',
className: '', className: '',
onHoverText: '', onHoverText: '',
intercomMessageDisabled: false,
}; };
export default FacingIssueBtn; export default FacingIssueBtn;

View File

@ -423,9 +423,9 @@ function AllErrors(): JSX.Element {
)?.tagValue; )?.tagValue;
logEvent('Exception: List page visited', { logEvent('Exception: List page visited', {
numberOfExceptions: errorCountResponse.data?.payload, numberOfExceptions: errorCountResponse?.data?.payload,
selectedEnvironments, selectedEnvironments,
resourceAttributeUsed: !!queries.length, resourceAttributeUsed: !!queries?.length,
}); });
logEventCalledRef.current = true; logEventCalledRef.current = true;
} }

View File

@ -19,10 +19,10 @@ import { ColumnsType } from 'antd/es/table';
import updateCreditCardApi from 'api/billing/checkout'; import updateCreditCardApi from 'api/billing/checkout';
import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage'; import getUsage, { UsageResponsePayloadProps } from 'api/billing/getUsage';
import manageCreditCardApi from 'api/billing/manage'; import manageCreditCardApi from 'api/billing/manage';
import logEvent from 'api/common/logEvent';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import useAnalytics from 'hooks/analytics/useAnalytics';
import useAxiosError from 'hooks/useAxiosError'; import useAxiosError from 'hooks/useAxiosError';
import useLicense from 'hooks/useLicense'; import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
@ -137,8 +137,6 @@ export default function BillingContainer(): JSX.Element {
Partial<UsageResponsePayloadProps> Partial<UsageResponsePayloadProps>
>({}); >({});
const { trackEvent } = useAnalytics();
const { isFetching, data: licensesData, error: licenseError } = useLicense(); const { isFetching, data: licensesData, error: licenseError } = useLicense();
const { user, org } = useSelector<AppState, AppReducer>((state) => state.app); const { user, org } = useSelector<AppState, AppReducer>((state) => state.app);
@ -316,7 +314,7 @@ export default function BillingContainer(): JSX.Element {
const handleBilling = useCallback(async () => { const handleBilling = useCallback(async () => {
if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) { if (isFreeTrial && !licensesData?.payload?.trialConvertedToSubscription) {
trackEvent('Billing : Upgrade Plan', { logEvent('Billing : Upgrade Plan', {
user: pick(user, ['email', 'userId', 'name']), user: pick(user, ['email', 'userId', 'name']),
org, org,
}); });
@ -327,7 +325,7 @@ export default function BillingContainer(): JSX.Element {
cancelURL: window.location.href, cancelURL: window.location.href,
}); });
} else { } else {
trackEvent('Billing : Manage Billing', { logEvent('Billing : Manage Billing', {
user: pick(user, ['email', 'userId', 'name']), user: pick(user, ['email', 'userId', 'name']),
org, org,
}); });

View File

@ -449,8 +449,8 @@ function CreateAlertChannels({
const result = await functionToCall(); const result = await functionToCall();
logEvent('Alert Channel: Save channel', { logEvent('Alert Channel: Save channel', {
type: value, type: value,
sendResolvedAlert: selectedConfig.send_resolved, sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig.name, name: selectedConfig?.name,
new: 'true', new: 'true',
status: result?.status, status: result?.status,
statusMessage: result?.statusMessage, statusMessage: result?.statusMessage,
@ -530,8 +530,8 @@ function CreateAlertChannels({
logEvent('Alert Channel: Test notification', { logEvent('Alert Channel: Test notification', {
type: channelType, type: channelType,
sendResolvedAlert: selectedConfig.send_resolved, sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig.name, name: selectedConfig?.name,
new: 'true', new: 'true',
status: status:
response && response.statusCode === 200 ? 'Test success' : 'Test failed', response && response.statusCode === 200 ? 'Test success' : 'Test failed',

View File

@ -370,8 +370,8 @@ function EditAlertChannels({
} }
logEvent('Alert Channel: Save channel', { logEvent('Alert Channel: Save channel', {
type: value, type: value,
sendResolvedAlert: selectedConfig.send_resolved, sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig.name, name: selectedConfig?.name,
new: 'false', new: 'false',
status: result?.status, status: result?.status,
statusMessage: result?.statusMessage, statusMessage: result?.statusMessage,
@ -441,8 +441,8 @@ function EditAlertChannels({
} }
logEvent('Alert Channel: Test notification', { logEvent('Alert Channel: Test notification', {
type: channelType, type: channelType,
sendResolvedAlert: selectedConfig.send_resolved, sendResolvedAlert: selectedConfig?.send_resolved,
name: selectedConfig.name, name: selectedConfig?.name,
new: 'false', new: 'false',
status: status:
response && response.statusCode === 200 ? 'Test success' : 'Test failed', response && response.statusCode === 200 ? 'Test success' : 'Test failed',

View File

@ -114,10 +114,10 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
const onClickTraceHandler = (): void => { const onClickTraceHandler = (): void => {
logEvent('Exception: Navigate to trace detail page', { logEvent('Exception: Navigate to trace detail page', {
groupId: errorDetail.groupID, groupId: errorDetail?.groupID,
spanId: errorDetail.spanID, spanId: errorDetail.spanID,
traceId: errorDetail.traceID, traceId: errorDetail.traceID,
exceptionId: errorDetail.errorId, exceptionId: errorDetail?.errorId,
}); });
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`); history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
}; };
@ -126,10 +126,10 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
useEffect(() => { useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) { if (!logEventCalledRef.current && !isUndefined(data)) {
logEvent('Exception: Detail page visited', { logEvent('Exception: Detail page visited', {
groupId: errorDetail.groupID, groupId: errorDetail?.groupID,
spanId: errorDetail.spanID, spanId: errorDetail.spanID,
traceId: errorDetail.traceID, traceId: errorDetail.traceID,
exceptionId: errorDetail.errorId, exceptionId: errorDetail?.errorId,
}); });
logEventCalledRef.current = true; logEventCalledRef.current = true;
} }

View File

@ -256,12 +256,12 @@ function ExplorerOptions({
if (sourcepage === DataSource.TRACES) { if (sourcepage === DataSource.TRACES) {
logEvent('Traces Explorer: Select view', { logEvent('Traces Explorer: Select view', {
panelType, panelType,
viewName: option.value, viewName: option?.value,
}); });
} else if (sourcepage === DataSource.LOGS) { } else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Explorer: Select view', { logEvent('Logs Explorer: Select view', {
panelType, panelType,
viewName: option.value, viewName: option?.value,
}); });
} }
if (ref.current) { if (ref.current) {

View File

@ -88,7 +88,7 @@ function BasicInfo({
if (!channels.loading && isNewRule) { if (!channels.loading && isNewRule) {
logEvent('Alert: New alert creation page visited', { logEvent('Alert: New alert creation page visited', {
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes], dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
numberOfChannels: channels.payload?.length, numberOfChannels: channels?.payload?.length,
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -125,10 +125,9 @@ function GridCardGraph({
offset: 0, offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0, limit: updatedQuery.builder.queryData[0].limit || 0,
}, },
// we do not need select columns in case of logs
selectColumns: selectColumns:
initialDataSource === DataSource.LOGS initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
? widget.selectedLogFields
: widget.selectedTracesFields,
}, },
fillGaps: widget.fillSpans, fillGaps: widget.fillSpans,
}; };

View File

@ -50,7 +50,7 @@
.footer { .footer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: absolute; position: fixed;
bottom: 0; bottom: 0;
width: -webkit-fill-available; width: -webkit-fill-available;

View File

@ -940,3 +940,50 @@
border-color: var(--bg-vanilla-300) !important; border-color: var(--bg-vanilla-300) !important;
} }
} }
.mt-8 {
margin-top: 8px;
}
.mt-12 {
margin-top: 12px;
}
.mt-24 {
margin-top: 24px;
}
.mb-24 {
margin-bottom: 24px;
}
.ingestion-setup-details-links {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 24px;
padding: 12px;
border-radius: 4px;
background: rgba(113, 144, 249, 0.1);
color: var(--bg-robin-300, #95acfb);
.learn-more {
display: inline-flex;
justify-content: center;
align-items: center;
text-decoration: underline;
color: var(--bg-robin-300, #95acfb);
}
}
.lightMode {
.ingestion-setup-details-links {
background: rgba(113, 144, 249, 0.1);
color: var(--bg-robin-500);
.learn-more {
color: var(--bg-robin-500);
}
}
}

View File

@ -34,11 +34,14 @@ import dayjs, { Dayjs } from 'dayjs';
import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys'; import { useGetAllIngestionsKeys } from 'hooks/IngestionKeys/useGetAllIngestionKeys';
import useDebouncedFn from 'hooks/useDebouncedFunction'; import useDebouncedFn from 'hooks/useDebouncedFunction';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { isNil } from 'lodash-es';
import { import {
ArrowUpRight,
CalendarClock, CalendarClock,
Check, Check,
Copy, Copy,
Infinity, Infinity,
Info,
Minus, Minus,
PenLine, PenLine,
Plus, Plus,
@ -603,243 +606,250 @@ function MultiIngestionSettings(): JSX.Element {
<div className="limits-data"> <div className="limits-data">
<div className="signals"> <div className="signals">
{SIGNALS.map((signal) => ( {SIGNALS.map((signal) => {
<div className="signal" key={signal}> const hasValidDayLimit = !isNil(limits[signal]?.config?.day?.size);
<div className="header"> const hasValidSecondLimit = !isNil(
<div className="signal-name">{signal}</div> limits[signal]?.config?.second?.size,
<div className="actions"> );
{hasLimits(signal) ? (
<> return (
<div className="signal" key={signal}>
<div className="header">
<div className="signal-name">{signal}</div>
<div className="actions">
{hasLimits(signal) ? (
<>
<Button
className="periscope-btn ghost"
icon={<PenLine size={14} />}
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
enableEditLimitMode(APIKey, limits[signal]);
}}
/>
<Button
className="periscope-btn ghost"
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />}
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
showDeleteLimitModal(APIKey, limits[signal]);
}}
/>
</>
) : (
<Button <Button
className="periscope-btn ghost" className="periscope-btn"
icon={<PenLine size={14} />} size="small"
shape="round"
icon={<PlusIcon size={14} />}
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)} disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
// eslint-disable-next-line sonarjs/no-identical-functions
onClick={(e): void => { onClick={(e): void => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
enableEditLimitMode(APIKey, limits[signal]);
}}
/>
<Button enableEditLimitMode(APIKey, {
className="periscope-btn ghost" id: signal,
icon={<Trash2 color={Color.BG_CHERRY_500} size={14} />} signal,
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)} config: {},
onClick={(e): void => { });
e.stopPropagation();
e.preventDefault();
showDeleteLimitModal(APIKey, limits[signal]);
}} }}
/> >
</> Limits
) : ( </Button>
<Button )}
className="periscope-btn" </div>
size="small" </div>
shape="round"
icon={<PlusIcon size={14} />}
disabled={!!(activeAPIKey?.id === APIKey.id && activeSignal)}
// eslint-disable-next-line sonarjs/no-identical-functions
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
enableEditLimitMode(APIKey, { <div className="signal-limit-values">
id: signal, {activeAPIKey?.id === APIKey.id &&
signal, activeSignal?.signal === signal &&
config: {}, isEditAddLimitOpen ? (
}); <Form
name="edit-ingestion-key-limit-form"
key="addEditLimitForm"
form={addEditLimitForm}
autoComplete="off"
initialValues={{
dailyLimit: bytesToGb(limits[signal]?.config?.day?.size),
secondsLimit: bytesToGb(limits[signal]?.config?.second?.size),
}} }}
className="edit-ingestion-key-limit-form"
> >
Limits <div className="signal-limit-edit-mode">
</Button> <div className="daily-limit">
<div className="heading">
<div className="title"> Daily limit </div>
<div className="subtitle">
Add a limit for data ingested daily{' '}
</div>
</div>
<div className="size">
<Form.Item name="dailyLimit">
<InputNumber
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB"> TiB</Option>
<Option value="GiB"> GiB</Option>
<Option value="MiB"> MiB </Option>
<Option value="KiB"> KiB </Option>
</Select>
}
/>
</Form.Item>
</div>
</div>
<div className="second-limit">
<div className="heading">
<div className="title"> Per Second limit </div>
<div className="subtitle">
{' '}
Add a limit for data ingested every second{' '}
</div>
</div>
<div className="size">
<Form.Item name="secondsLimit">
<InputNumber
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB"> TiB</Option>
<Option value="GiB"> GiB</Option>
<Option value="MiB"> MiB </Option>
<Option value="KiB"> KiB </Option>
</Select>
}
/>
</Form.Item>
</div>
</div>
</div>
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
!isLoadingLimitForKey &&
hasCreateLimitForIngestionKeyError &&
createLimitForIngestionKeyError &&
createLimitForIngestionKeyError?.error && (
<div className="error">
{createLimitForIngestionKeyError?.error}
</div>
)}
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
!isLoadingLimitForKey &&
hasUpdateLimitForIngestionKeyError &&
updateLimitForIngestionKeyError && (
<div className="error">
{updateLimitForIngestionKeyError?.error}
</div>
)}
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
isEditAddLimitOpen && (
<div className="signal-limit-save-discard">
<Button
type="primary"
className="periscope-btn primary"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
loading={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={(): void => {
if (!hasLimits(signal)) {
handleAddLimit(APIKey, signal);
} else {
handleUpdateLimit(APIKey, limits[signal]);
}
}}
>
Save
</Button>
<Button
type="default"
className="periscope-btn"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={handleDiscardSaveLimit}
>
Discard
</Button>
</div>
)}
</Form>
) : (
<div className="signal-limit-view-mode">
<div className="signal-limit-value">
<div className="limit-type">
Daily <Minus size={16} />{' '}
</div>
<div className="limit-value">
{hasValidDayLimit ? (
<>
{getYAxisFormattedValue(
(limits[signal]?.metric?.day?.size || 0).toString(),
'bytes',
)}{' '}
/{' '}
{getYAxisFormattedValue(
(limits[signal]?.config?.day?.size || 0).toString(),
'bytes',
)}
</>
) : (
<>
<Infinity size={16} /> NO LIMIT
</>
)}
</div>
</div>
<div className="signal-limit-value">
<div className="limit-type">
Seconds <Minus size={16} />
</div>
<div className="limit-value">
{hasValidSecondLimit ? (
<>
{getYAxisFormattedValue(
(limits[signal]?.metric?.second?.size || 0).toString(),
'bytes',
)}{' '}
/{' '}
{getYAxisFormattedValue(
(limits[signal]?.config?.second?.size || 0).toString(),
'bytes',
)}
</>
) : (
<>
<Infinity size={16} /> NO LIMIT
</>
)}
</div>
</div>
</div>
)} )}
</div> </div>
</div> </div>
);
<div className="signal-limit-values"> })}
{activeAPIKey?.id === APIKey.id &&
activeSignal?.signal === signal &&
isEditAddLimitOpen ? (
<Form
name="edit-ingestion-key-limit-form"
key="addEditLimitForm"
form={addEditLimitForm}
autoComplete="off"
initialValues={{
dailyLimit: bytesToGb(limits[signal]?.config?.day?.size),
secondsLimit: bytesToGb(limits[signal]?.config?.second?.size),
}}
className="edit-ingestion-key-limit-form"
>
<div className="signal-limit-edit-mode">
<div className="daily-limit">
<div className="heading">
<div className="title"> Daily limit </div>
<div className="subtitle">
Add a limit for data ingested daily{' '}
</div>
</div>
<div className="size">
<Form.Item name="dailyLimit">
<InputNumber
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB"> TiB</Option>
<Option value="GiB"> GiB</Option>
<Option value="MiB"> MiB </Option>
<Option value="KiB"> KiB </Option>
</Select>
}
/>
</Form.Item>
</div>
</div>
<div className="second-limit">
<div className="heading">
<div className="title"> Per Second limit </div>
<div className="subtitle">
{' '}
Add a limit for data ingested every second{' '}
</div>
</div>
<div className="size">
<Form.Item name="secondsLimit">
<InputNumber
addonAfter={
<Select defaultValue="GiB" disabled>
<Option value="TiB"> TiB</Option>
<Option value="GiB"> GiB</Option>
<Option value="MiB"> MiB </Option>
<Option value="KiB"> KiB </Option>
</Select>
}
/>
</Form.Item>
</div>
</div>
</div>
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
!isLoadingLimitForKey &&
hasCreateLimitForIngestionKeyError &&
createLimitForIngestionKeyError &&
createLimitForIngestionKeyError?.error && (
<div className="error">
{createLimitForIngestionKeyError?.error}
</div>
)}
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
!isLoadingLimitForKey &&
hasUpdateLimitForIngestionKeyError &&
updateLimitForIngestionKeyError && (
<div className="error">
{updateLimitForIngestionKeyError?.error}
</div>
)}
{activeAPIKey?.id === APIKey.id &&
activeSignal.signal === signal &&
isEditAddLimitOpen && (
<div className="signal-limit-save-discard">
<Button
type="primary"
className="periscope-btn primary"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
loading={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={(): void => {
if (!hasLimits(signal)) {
handleAddLimit(APIKey, signal);
} else {
handleUpdateLimit(APIKey, limits[signal]);
}
}}
>
Save
</Button>
<Button
type="default"
className="periscope-btn"
size="small"
disabled={
isLoadingLimitForKey || isLoadingUpdatedLimitForKey
}
onClick={handleDiscardSaveLimit}
>
Discard
</Button>
</div>
)}
</Form>
) : (
<div className="signal-limit-view-mode">
<div className="signal-limit-value">
<div className="limit-type">
Daily <Minus size={16} />{' '}
</div>
<div className="limit-value">
{limits[signal]?.config?.day?.size ? (
<>
{getYAxisFormattedValue(
(limits[signal]?.metric?.day?.size || 0).toString(),
'bytes',
)}{' '}
/{' '}
{getYAxisFormattedValue(
(limits[signal]?.config?.day?.size || 0).toString(),
'bytes',
)}
</>
) : (
<>
<Infinity size={16} /> NO LIMIT
</>
)}
</div>
</div>
<div className="signal-limit-value">
<div className="limit-type">
Seconds <Minus size={16} />
</div>
<div className="limit-value">
{limits[signal]?.config?.second?.size ? (
<>
{getYAxisFormattedValue(
(limits[signal]?.metric?.second?.size || 0).toString(),
'bytes',
)}{' '}
/{' '}
{getYAxisFormattedValue(
(limits[signal]?.config?.second?.size || 0).toString(),
'bytes',
)}
</>
) : (
<>
<Infinity size={16} /> NO LIMIT
</>
)}
</div>
</div>
</div>
)}
</div>
</div>
))}
</div> </div>
</div> </div>
</div> </div>
@ -875,10 +885,35 @@ function MultiIngestionSettings(): JSX.Element {
return ( return (
<div className="ingestion-key-container"> <div className="ingestion-key-container">
<div className="ingestion-key-content"> <div className="ingestion-key-content">
<div className="ingestion-setup-details-links">
<Info size={14} />
<span>
Find your ingestion URL and learn more about sending data to SigNoz{' '}
<a
href="https://signoz.io/docs/ingestion/signoz-cloud/overview/"
target="_blank"
className="learn-more"
rel="noreferrer"
>
here <ArrowUpRight size={14} />
</a>
</span>
</div>
<header> <header>
<Typography.Title className="title"> Ingestion Keys </Typography.Title> <Typography.Title className="title"> Ingestion Keys </Typography.Title>
<Typography.Text className="subtitle"> <Typography.Text className="subtitle">
Create and manage ingestion keys for the SigNoz Cloud Create and manage ingestion keys for the SigNoz Cloud{' '}
<a
href="https://signoz.io/docs/ingestion/signoz-cloud/keys/"
target="_blank"
className="learn-more"
rel="noreferrer"
>
{' '}
Learn more <ArrowUpRight size={14} />
</a>
</Typography.Text> </Typography.Text>
</header> </header>

View File

@ -0,0 +1,45 @@
import { render, screen } from 'tests/test-utils';
import MultiIngestionSettings from '../MultiIngestionSettings';
describe('MultiIngestionSettings Page', () => {
beforeEach(() => {
render(<MultiIngestionSettings />);
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders MultiIngestionSettings page without crashing', () => {
expect(
screen.getByText(
'Find your ingestion URL and learn more about sending data to SigNoz',
),
).toBeInTheDocument();
expect(screen.getByText('Ingestion Keys')).toBeInTheDocument();
expect(
screen.getByText('Create and manage ingestion keys for the SigNoz Cloud'),
).toBeInTheDocument();
const overviewLink = screen.getByRole('link', { name: /here/i });
expect(overviewLink).toHaveAttribute(
'href',
'https://signoz.io/docs/ingestion/signoz-cloud/overview/',
);
expect(overviewLink).toHaveAttribute('target', '_blank');
expect(overviewLink).toHaveClass('learn-more');
expect(overviewLink).toHaveAttribute('rel', 'noreferrer');
const aboutKeyslink = screen.getByRole('link', { name: /Learn more/i });
expect(aboutKeyslink).toHaveAttribute(
'href',
'https://signoz.io/docs/ingestion/signoz-cloud/keys/',
);
expect(aboutKeyslink).toHaveAttribute('target', '_blank');
expect(aboutKeyslink).toHaveClass('learn-more');
expect(aboutKeyslink).toHaveAttribute('rel', 'noreferrer');
});
});

View File

@ -49,9 +49,9 @@ export const alertActionLogEvent = (
break; break;
} }
logEvent('Alert: Action', { logEvent('Alert: Action', {
ruleId: record.id, ruleId: record?.id,
dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes], dataSource: ALERTS_DATA_SOURCE_MAP[record.alertType as AlertTypes],
name: record.alert, name: record?.alert,
action: actionValue, action: actionValue,
}); });
}; };

View File

@ -73,7 +73,6 @@ import { Dashboard } from 'types/api/dashboard/getAll';
import AppReducer from 'types/reducer/app'; import AppReducer from 'types/reducer/app';
import { isCloudUser } from 'utils/app'; import { isCloudUser } from 'utils/app';
import useUrlQuery from '../../hooks/useUrlQuery';
import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal'; import DashboardTemplatesModal from './DashboardTemplates/DashboardTemplatesModal';
import ImportJSON from './ImportJSON'; import ImportJSON from './ImportJSON';
import { DeleteButton } from './TableComponents/DeleteButton'; import { DeleteButton } from './TableComponents/DeleteButton';
@ -86,7 +85,7 @@ import {
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardsList(): JSX.Element { function DashboardsList(): JSX.Element {
const { const {
data: dashboardListResponse = [], data: dashboardListResponse,
isLoading: isDashboardListLoading, isLoading: isDashboardListLoading,
error: dashboardFetchError, error: dashboardFetchError,
refetch: refetchDashboardList, refetch: refetchDashboardList,
@ -99,12 +98,14 @@ function DashboardsList(): JSX.Element {
setListSortOrder: setSortOrder, setListSortOrder: setSortOrder,
} = useDashboard(); } = useDashboard();
const [searchString, setSearchString] = useState<string>(
sortOrder.search || '',
);
const [action, createNewDashboard] = useComponentPermission( const [action, createNewDashboard] = useComponentPermission(
['action', 'create_new_dashboards'], ['action', 'create_new_dashboards'],
role, role,
); );
const [searchValue, setSearchValue] = useState<string>('');
const [ const [
showNewDashboardTemplatesModal, showNewDashboardTemplatesModal,
setShowNewDashboardTemplatesModal, setShowNewDashboardTemplatesModal,
@ -123,10 +124,6 @@ function DashboardsList(): JSX.Element {
false, false,
); );
const params = useUrlQuery();
const searchParams = params.get('search');
const [searchString, setSearchString] = useState<string>(searchParams || '');
const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => { const getLocalStorageDynamicColumns = (): DashboardDynamicColumns => {
const dashboardDynamicColumnsString = localStorage.getItem('dashboard'); const dashboardDynamicColumnsString = localStorage.getItem('dashboard');
let dashboardDynamicColumns: DashboardDynamicColumns = { let dashboardDynamicColumns: DashboardDynamicColumns = {
@ -188,14 +185,6 @@ function DashboardsList(): JSX.Element {
setDashboards(sortedDashboards); setDashboards(sortedDashboards);
}; };
useEffect(() => {
params.set('columnKey', sortOrder.columnKey as string);
params.set('order', sortOrder.order as string);
params.set('page', sortOrder.pagination || '1');
history.replace({ search: params.toString() });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sortOrder]);
const sortHandle = (key: string): void => { const sortHandle = (key: string): void => {
if (!dashboards) return; if (!dashboards) return;
if (key === 'createdAt') { if (key === 'createdAt') {
@ -204,6 +193,7 @@ function DashboardsList(): JSX.Element {
columnKey: 'createdAt', columnKey: 'createdAt',
order: 'descend', order: 'descend',
pagination: sortOrder.pagination || '1', pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
}); });
} else if (key === 'updatedAt') { } else if (key === 'updatedAt') {
sortDashboardsByUpdatedAt(dashboards); sortDashboardsByUpdatedAt(dashboards);
@ -211,21 +201,19 @@ function DashboardsList(): JSX.Element {
columnKey: 'updatedAt', columnKey: 'updatedAt',
order: 'descend', order: 'descend',
pagination: sortOrder.pagination || '1', pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
}); });
} }
}; };
function handlePageSizeUpdate(page: number): void { function handlePageSizeUpdate(page: number): void {
setSortOrder((order) => ({ setSortOrder({ ...sortOrder, pagination: String(page) });
...order,
pagination: String(page),
}));
} }
useEffect(() => { useEffect(() => {
const filteredDashboards = filterDashboard( const filteredDashboards = filterDashboard(
searchString, searchString,
dashboardListResponse, dashboardListResponse || [],
); );
if (sortOrder.columnKey === 'updatedAt') { if (sortOrder.columnKey === 'updatedAt') {
sortDashboardsByUpdatedAt(filteredDashboards || []); sortDashboardsByUpdatedAt(filteredDashboards || []);
@ -236,6 +224,7 @@ function DashboardsList(): JSX.Element {
columnKey: 'updatedAt', columnKey: 'updatedAt',
order: 'descend', order: 'descend',
pagination: sortOrder.pagination || '1', pagination: sortOrder.pagination || '1',
search: sortOrder.search || '',
}); });
sortDashboardsByUpdatedAt(filteredDashboards || []); sortDashboardsByUpdatedAt(filteredDashboards || []);
} }
@ -245,6 +234,7 @@ function DashboardsList(): JSX.Element {
setSortOrder, setSortOrder,
sortOrder.columnKey, sortOrder.columnKey,
sortOrder.pagination, sortOrder.pagination,
sortOrder.search,
]); ]);
const [newDashboardState, setNewDashboardState] = useState({ const [newDashboardState, setNewDashboardState] = useState({
@ -316,12 +306,15 @@ function DashboardsList(): JSX.Element {
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => { const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
setIsFilteringDashboards(true); setIsFilteringDashboards(true);
setSearchValue(event.target.value);
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || ''; const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
const filteredDashboards = filterDashboard(searchText, dashboardListResponse); const filteredDashboards = filterDashboard(
searchText,
dashboardListResponse || [],
);
setDashboards(filteredDashboards); setDashboards(filteredDashboards);
setIsFilteringDashboards(false); setIsFilteringDashboards(false);
setSearchString(searchText); setSearchString(searchText);
setSortOrder({ ...sortOrder, search: searchText });
}; };
const [state, setCopy] = useCopyToClipboard(); const [state, setCopy] = useCopyToClipboard();
@ -412,7 +405,7 @@ function DashboardsList(): JSX.Element {
{ {
title: 'Dashboards', title: 'Dashboards',
key: 'dashboard', key: 'dashboard',
render: (dashboard: Data): JSX.Element => { render: (dashboard: Data, _, index): JSX.Element => {
const timeOptions: Intl.DateTimeFormatOptions = { const timeOptions: Intl.DateTimeFormatOptions = {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
@ -461,7 +454,9 @@ function DashboardsList(): JSX.Element {
style={{ height: '14px', width: '14px' }} style={{ height: '14px', width: '14px' }}
alt="dashboard-image" alt="dashboard-image"
/> />
<Typography.Text>{dashboard.name}</Typography.Text> <Typography.Text data-testid={`dashboard-title-${index}`}>
{dashboard.name}
</Typography.Text>
</div> </div>
<div className="tags-with-actions"> <div className="tags-with-actions">
@ -658,8 +653,9 @@ function DashboardsList(): JSX.Element {
}} }}
eventName="Dashboard: Facing Issues in dashboard" eventName="Dashboard: Facing Issues in dashboard"
message={dashboardListMessage} message={dashboardListMessage}
buttonText="Facing issues with dashboards?" buttonText="Need help with dashboards?"
onHoverText="Click here to get help with dashboards" onHoverText="Click here to get help with dashboards"
intercomMessageDisabled
/> />
</Flex> </Flex>
</div> </div>
@ -701,7 +697,7 @@ function DashboardsList(): JSX.Element {
<ArrowUpRight size={16} className="learn-more-arrow" /> <ArrowUpRight size={16} className="learn-more-arrow" />
</section> </section>
</div> </div>
) : dashboards?.length === 0 && !searchValue ? ( ) : dashboards?.length === 0 && !searchString ? (
<div className="dashboard-empty-state"> <div className="dashboard-empty-state">
<img <img
src="/Icons/dashboards.svg" src="/Icons/dashboards.svg"
@ -739,6 +735,7 @@ function DashboardsList(): JSX.Element {
<Button <Button
type="text" type="text"
className="learn-more" className="learn-more"
data-testid="learn-more"
onClick={(): void => { onClick={(): void => {
window.open( window.open(
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state', 'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
@ -758,7 +755,7 @@ function DashboardsList(): JSX.Element {
<Input <Input
placeholder="Search by name, description, or tags..." placeholder="Search by name, description, or tags..."
prefix={<Search size={12} color={Color.BG_VANILLA_400} />} prefix={<Search size={12} color={Color.BG_VANILLA_400} />}
value={searchValue} value={searchString}
onChange={handleSearch} onChange={handleSearch}
/> />
{createNewDashboard && ( {createNewDashboard && (
@ -786,7 +783,7 @@ function DashboardsList(): JSX.Element {
<div className="no-search"> <div className="no-search">
<img src="/Icons/emptyState.svg" alt="img" className="img" /> <img src="/Icons/emptyState.svg" alt="img" className="img" />
<Typography.Text className="text"> <Typography.Text className="text">
No dashboards found for {searchValue}. Create a new dashboard? No dashboards found for {searchString}. Create a new dashboard?
</Typography.Text> </Typography.Text>
</div> </div>
) : ( ) : (
@ -808,6 +805,7 @@ function DashboardsList(): JSX.Element {
type="text" type="text"
className={cx('sort-btns')} className={cx('sort-btns')}
onClick={(): void => sortHandle('createdAt')} onClick={(): void => sortHandle('createdAt')}
data-testid="sort-by-last-created"
> >
Last created Last created
{sortOrder.columnKey === 'createdAt' && <Check size={14} />} {sortOrder.columnKey === 'createdAt' && <Check size={14} />}
@ -816,6 +814,7 @@ function DashboardsList(): JSX.Element {
type="text" type="text"
className={cx('sort-btns')} className={cx('sort-btns')}
onClick={(): void => sortHandle('updatedAt')} onClick={(): void => sortHandle('updatedAt')}
data-testid="sort-by-last-updated"
> >
Last updated Last updated
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />} {sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
@ -826,7 +825,7 @@ function DashboardsList(): JSX.Element {
placement="bottomRight" placement="bottomRight"
arrow={false} arrow={false}
> >
<ArrowDownWideNarrow size={14} /> <ArrowDownWideNarrow size={14} data-testid="sort-by" />
</Popover> </Popover>
</Tooltip> </Tooltip>
<Popover <Popover

View File

@ -108,7 +108,7 @@ function DBCall(): JSX.Element {
logEvent('APM: Service detail page visited', { logEvent('APM: Service detail page visited', {
selectedEnvironments, selectedEnvironments,
resourceAttributeUsed: !!queries.length, resourceAttributeUsed: !!queries?.length,
section: 'dbMetrics', section: 'dbMetrics',
}); });
logEventCalledRef.current = true; logEventCalledRef.current = true;

View File

@ -124,7 +124,7 @@ function External(): JSX.Element {
logEvent('APM: Service detail page visited', { logEvent('APM: Service detail page visited', {
selectedEnvironments, selectedEnvironments,
resourceAttributeUsed: !!queries.length, resourceAttributeUsed: !!queries?.length,
section: 'externalMetrics', section: 'externalMetrics',
}); });
logEventCalledRef.current = true; logEventCalledRef.current = true;

View File

@ -91,7 +91,7 @@ function Application(): JSX.Element {
logEvent('APM: Service detail page visited', { logEvent('APM: Service detail page visited', {
selectedEnvironments, selectedEnvironments,
resourceAttributeUsed: !!queries.length, resourceAttributeUsed: !!queries?.length,
section: 'overview', section: 'overview',
}); });
logEventCalledRef.current = true; logEventCalledRef.current = true;

View File

@ -0,0 +1,100 @@
import { getNonIntegrationDashboardById } from 'mocks-server/__mockdata__/dashboards';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
import DashboardDescription from '..';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
useRouteMatch: jest.fn().mockReturnValue({
params: {
dashboardId: 4,
},
}),
}));
jest.mock(
'container/TopNav/DateTimeSelectionV2/index.tsx',
() =>
function MockDateTimeSelection(): JSX.Element {
return <div>MockDateTimeSelection</div>;
},
);
describe('Dashboard landing page actions header tests', () => {
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/dashboard/4`,
search: '',
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByTestId } = render(
<MemoryRouter initialEntries={['/dashboard/4']}>
<DashboardProvider>
<DashboardDescription
handle={{
active: false,
enter: (): Promise<void> => Promise.resolve(),
exit: (): Promise<void> => Promise.resolve(),
node: { current: null },
}}
/>
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() =>
expect(getByTestId('dashboard-title')).toHaveTextContent('thor'),
);
const dashboardSettingsTrigger = getByTestId('options');
await fireEvent.click(dashboardSettingsTrigger);
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
await waitFor(() => expect(lockUnlockButton).toBeDisabled());
});
it('unlock dashboard should not be disabled for non integration created dashboards', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/dashboard/4`,
search: '',
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
server.use(
rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) =>
res(ctx.status(200), ctx.json(getNonIntegrationDashboardById)),
),
);
const { getByTestId } = render(
<MemoryRouter initialEntries={['/dashboard/4']}>
<DashboardProvider>
<DashboardDescription
handle={{
active: false,
enter: (): Promise<void> => Promise.resolve(),
exit: (): Promise<void> => Promise.resolve(),
node: { current: null },
}}
/>
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() =>
expect(getByTestId('dashboard-title')).toHaveTextContent('thor'),
);
const dashboardSettingsTrigger = getByTestId('options');
await fireEvent.click(dashboardSettingsTrigger);
const lockUnlockButton = screen.getByTestId('lock-unlock-dashboard');
await waitFor(() => expect(lockUnlockButton).not.toBeDisabled());
});
});

View File

@ -1,7 +1,16 @@
import './Description.styles.scss'; import './Description.styles.scss';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { Button, Card, Input, Modal, Popover, Tag, Typography } from 'antd'; import {
Button,
Card,
Input,
Modal,
Popover,
Tag,
Tooltip,
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn'; import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { dashboardHelpMessage } from 'components/facingIssueBtn/util'; import { dashboardHelpMessage } from 'components/facingIssueBtn/util';
@ -266,6 +275,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
urlQuery.set('columnKey', listSortOrder.columnKey as string); urlQuery.set('columnKey', listSortOrder.columnKey as string);
urlQuery.set('order', listSortOrder.order as string); urlQuery.set('order', listSortOrder.order as string);
urlQuery.set('page', listSortOrder.pagination as string); urlQuery.set('page', listSortOrder.pagination as string);
urlQuery.set('search', listSortOrder.search as string);
urlQuery.delete(QueryParams.relativeTime); urlQuery.delete(QueryParams.relativeTime);
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`; const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlQuery.toString()}`;
@ -299,17 +309,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
{title} {title}
</Button> </Button>
</section> </section>
<FacingIssueBtn
attributes={{
uuid: selectedDashboard?.uuid,
title: updatedTitle,
screen: 'Dashboard Details',
}}
eventName="Dashboard: Facing Issues in dashboard"
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
buttonText="Facing issues with dashboards?"
onHoverText="Click here to get help with dashboard details"
/>
</div> </div>
<section className="dashbord-details"> <section className="dashbord-details">
<div className="left-section"> <div className="left-section">
@ -318,10 +317,24 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
alt="dashboard-img" alt="dashboard-img"
style={{ width: '16px', height: '16px' }} style={{ width: '16px', height: '16px' }}
/> />
<Typography.Text className="dashboard-title">{title}</Typography.Text> <Typography.Text className="dashboard-title" data-testid="dashboard-title">
{title}
</Typography.Text>
{isDashboardLocked && <LockKeyhole size={14} />} {isDashboardLocked && <LockKeyhole size={14} />}
</div> </div>
<div className="right-section"> <div className="right-section">
<FacingIssueBtn
attributes={{
uuid: selectedDashboard?.uuid,
title: updatedTitle,
screen: 'Dashboard Details',
}}
eventName="Dashboard: Facing Issues in dashboard"
message={dashboardHelpMessage(selectedDashboard?.data, selectedDashboard)}
buttonText="Need help with this dashboard?"
onHoverText="Click here to get help with dashboard"
intercomMessageDisabled
/>
<DateTimeSelectionV2 showAutoRefresh hideShareModal /> <DateTimeSelectionV2 showAutoRefresh hideShareModal />
<Popover <Popover
open={isDashboardSettingsOpen} open={isDashboardSettingsOpen}
@ -332,13 +345,22 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<div className="menu-content"> <div className="menu-content">
<section className="section-1"> <section className="section-1">
{(isAuthor || role === USER_ROLES.ADMIN) && ( {(isAuthor || role === USER_ROLES.ADMIN) && (
<Button <Tooltip
type="text" title={
icon={<LockKeyhole size={14} />} selectedDashboard?.created_by === 'integration' &&
onClick={handleLockDashboardToggle} 'Dashboards created by integrations cannot be unlocked'
}
> >
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'} <Button
</Button> type="text"
icon={<LockKeyhole size={14} />}
disabled={selectedDashboard?.created_by === 'integration'}
onClick={handleLockDashboardToggle}
data-testid="lock-unlock-dashboard"
>
{isDashboardLocked ? 'Unlock Dashboard' : 'Lock Dashboard'}
</Button>
</Tooltip>
)} )}
{!isDashboardLocked && editDashboard && ( {!isDashboardLocked && editDashboard && (

View File

@ -309,6 +309,7 @@ function ExplorerColumnsRenderer({
> >
<Button <Button
className="action-btn" className="action-btn"
data-testid="add-columns-button"
icon={ icon={
<PlusCircle <PlusCircle
size={16} size={16}

View File

@ -4,6 +4,7 @@ import { Color } from '@signozhq/design-tokens';
import { Button, Tabs, Tooltip, Typography } from 'antd'; import { Button, Tabs, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import PromQLIcon from 'assets/Dashboard/PromQl'; import PromQLIcon from 'assets/Dashboard/PromQl';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts'; import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
@ -236,6 +237,21 @@ function QuerySection({
onChange={handleQueryCategoryChange} onChange={handleQueryCategoryChange}
tabBarExtraContent={ tabBarExtraContent={
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}> <span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
<FacingIssueBtn
attributes={{
uuid: selectedDashboard?.uuid,
title: selectedDashboard?.data.title,
screen: 'Dashboard widget',
panelType: selectedGraph,
widgetId: query.id,
queryType: currentQuery.queryType,
}}
eventName="Dashboard: Facing Issues in dashboard"
buttonText="Need help with this chart?"
// message={chartHelpMessage(selectedDashboard, graphType)}
onHoverText="Click here to get help with this dashboard widget"
intercomMessageDisabled
/>
<TextToolTip <TextToolTip
text="This will temporarily save the current query and graph state. This will persist across tab change" text="This will temporarily save the current query and graph state. This will persist across tab change"
url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder" url="https://signoz.io/docs/userguide/query-builder?utm_source=product&utm_medium=query-builder"

View File

@ -4,8 +4,6 @@ import './NewWidget.styles.scss';
import { WarningOutlined } from '@ant-design/icons'; import { WarningOutlined } from '@ant-design/icons';
import { Button, Flex, Modal, Space, Tooltip, Typography } from 'antd'; import { Button, Flex, Modal, Space, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import FacingIssueBtn from 'components/facingIssueBtn/FacingIssueBtn';
import { chartHelpMessage } from 'components/facingIssueBtn/util';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query'; import { QueryParams } from 'constants/query';
@ -79,6 +77,7 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
stagedQuery, stagedQuery,
redirectWithQueryBuilderData, redirectWithQueryBuilderData,
supersetQuery, supersetQuery,
setSupersetQuery,
} = useQueryBuilder(); } = useQueryBuilder();
const isQueryModified = useMemo( const isQueryModified = useMemo(
@ -548,6 +547,17 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
isNewTraceLogsAvailable, isNewTraceLogsAvailable,
]); ]);
useEffect(() => {
/**
* we need this extra handling for superset query because we cannot keep this in sync with current query
* always.we do not sync superset query in the initQueryBuilderData because that function is called on all stage and run
* actions. we do not want that as we loose out on superset functionalities if we do the same. hence initialising the superset query
* on mount here with the currentQuery in the begining itself
*/
setSupersetQuery(currentQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => { useEffect(() => {
registerShortcut(DashboardShortcuts.SaveChanges, onSaveDashboard); registerShortcut(DashboardShortcuts.SaveChanges, onSaveDashboard);
registerShortcut(DashboardShortcuts.DiscardChanges, onClickDiscardHandler); registerShortcut(DashboardShortcuts.DiscardChanges, onClickDiscardHandler);
@ -563,11 +573,11 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
if (selectedGraph === PANEL_TYPES.LIST) { if (selectedGraph === PANEL_TYPES.LIST) {
const initialDataSource = currentQuery.builder.queryData[0].dataSource; const initialDataSource = currentQuery.builder.queryData[0].dataSource;
if (initialDataSource === DataSource.LOGS) { if (initialDataSource === DataSource.LOGS) {
// we do not need selected log columns in the request data as the entire response contains all the necessary data
setRequestData((prev) => ({ setRequestData((prev) => ({
...prev, ...prev,
tableParams: { tableParams: {
...prev.tableParams, ...prev.tableParams,
selectColumns: selectedLogFields,
}, },
})); }));
} else if (initialDataSource === DataSource.TRACES) { } else if (initialDataSource === DataSource.TRACES) {
@ -596,20 +606,6 @@ function NewWidget({ selectedGraph }: NewWidgetProps): JSX.Element {
<Typography.Text className="configure-panel"> <Typography.Text className="configure-panel">
Configure panel Configure panel
</Typography.Text> </Typography.Text>
<FacingIssueBtn
attributes={{
uuid: selectedDashboard?.uuid,
title: selectedDashboard?.data.title,
screen: 'Dashboard widget',
panelType: graphType,
widgetId: query.get('widgetId'),
queryType: currentQuery.queryType,
}}
eventName="Dashboard: Facing Issues in dashboard"
message={chartHelpMessage(selectedDashboard, graphType)}
buttonText="Facing issues with dashboards?"
onHoverText="Click here to get help with dashboard widget"
/>
</Flex> </Flex>
</div> </div>
{isSaveDisabled && ( {isSaveDisabled && (

View File

@ -11,7 +11,6 @@ import ROUTES from 'constants/routes';
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader'; import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal'; import InviteUserModal from 'container/OrganizationSettings/InviteUserModal/InviteUserModal';
import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer'; import { InviteMemberFormValues } from 'container/OrganizationSettings/PendingInvitesContainer';
import useAnalytics from 'hooks/analytics/useAnalytics';
import history from 'lib/history'; import history from 'lib/history';
import { UserPlus } from 'lucide-react'; import { UserPlus } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
@ -104,7 +103,6 @@ export default function Onboarding(): JSX.Element {
const [selectedModuleSteps, setSelectedModuleSteps] = useState(APM_STEPS); const [selectedModuleSteps, setSelectedModuleSteps] = useState(APM_STEPS);
const [activeStep, setActiveStep] = useState(1); const [activeStep, setActiveStep] = useState(1);
const [current, setCurrent] = useState(0); const [current, setCurrent] = useState(0);
const { trackEvent } = useAnalytics();
const { location } = history; const { location } = history;
const { t } = useTranslation(['onboarding']); const { t } = useTranslation(['onboarding']);
@ -120,7 +118,7 @@ export default function Onboarding(): JSX.Element {
} = useOnboardingContext(); } = useOnboardingContext();
useEffectOnce(() => { useEffectOnce(() => {
trackEvent('Onboarding V2 Started'); logEvent('Onboarding V2 Started', {});
}); });
const { status, data: ingestionData } = useQuery({ const { status, data: ingestionData } = useQuery({
@ -231,7 +229,7 @@ export default function Onboarding(): JSX.Element {
const nextStep = activeStep + 1; const nextStep = activeStep + 1;
// on next // on next
trackEvent('Onboarding V2: Get Started', { logEvent('Onboarding V2: Get Started', {
selectedModule: selectedModule.id, selectedModule: selectedModule.id,
nextStepId: nextStep, nextStepId: nextStep,
}); });

View File

@ -5,9 +5,9 @@ import {
CloseCircleTwoTone, CloseCircleTwoTone,
LoadingOutlined, LoadingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import logEvent from 'api/common/logEvent';
import Header from 'container/OnboardingContainer/common/Header/Header'; import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext'; import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useQueryService } from 'hooks/useQueryService'; import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute'; import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils'; import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
@ -41,8 +41,6 @@ export default function ConnectionStatus(): JSX.Element {
[queries], [queries],
); );
const { trackEvent } = useAnalytics();
const [retryCount, setRetryCount] = useState(20); // Retry for 3 mins 20s const [retryCount, setRetryCount] = useState(20); // Retry for 3 mins 20s
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isReceivingData, setIsReceivingData] = useState(false); const [isReceivingData, setIsReceivingData] = useState(false);
@ -155,7 +153,7 @@ export default function ConnectionStatus(): JSX.Element {
if (data || isError) { if (data || isError) {
setRetryCount(retryCount - 1); setRetryCount(retryCount - 1);
if (retryCount < 0) { if (retryCount < 0) {
trackEvent('Onboarding V2: Connection Status', { logEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
framework: selectedFramework, framework: selectedFramework,
environment: selectedEnvironment, environment: selectedEnvironment,
@ -174,7 +172,7 @@ export default function ConnectionStatus(): JSX.Element {
setLoading(false); setLoading(false);
setIsReceivingData(true); setIsReceivingData(true);
trackEvent('Onboarding V2: Connection Status', { logEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
framework: selectedFramework, framework: selectedFramework,
environment: selectedEnvironment, environment: selectedEnvironment,

View File

@ -5,11 +5,11 @@ import {
CloseCircleTwoTone, CloseCircleTwoTone,
LoadingOutlined, LoadingOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION } from 'constants/app'; import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder'; import { PANEL_TYPES } from 'constants/queryBuilder';
import Header from 'container/OnboardingContainer/common/Header/Header'; import Header from 'container/OnboardingContainer/common/Header/Header';
import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext'; import { useOnboardingContext } from 'container/OnboardingContainer/context/OnboardingContext';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { SuccessResponse } from 'types/api'; import { SuccessResponse } from 'types/api';
@ -32,7 +32,6 @@ export default function LogsConnectionStatus(): JSX.Element {
activeStep, activeStep,
selectedEnvironment, selectedEnvironment,
} = useOnboardingContext(); } = useOnboardingContext();
const { trackEvent } = useAnalytics();
const [isReceivingData, setIsReceivingData] = useState(false); const [isReceivingData, setIsReceivingData] = useState(false);
const [pollingInterval, setPollingInterval] = useState<number | false>(15000); // initial Polling interval of 15 secs , Set to false after 5 mins const [pollingInterval, setPollingInterval] = useState<number | false>(15000); // initial Polling interval of 15 secs , Set to false after 5 mins
const [retryCount, setRetryCount] = useState(20); // Retry for 5 mins const [retryCount, setRetryCount] = useState(20); // Retry for 5 mins
@ -105,7 +104,7 @@ export default function LogsConnectionStatus(): JSX.Element {
setRetryCount(retryCount - 1); setRetryCount(retryCount - 1);
if (retryCount < 0) { if (retryCount < 0) {
trackEvent('Onboarding V2: Connection Status', { logEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
@ -141,7 +140,7 @@ export default function LogsConnectionStatus(): JSX.Element {
setRetryCount(-1); setRetryCount(-1);
setPollingInterval(false); setPollingInterval(false);
trackEvent('Onboarding V2: Connection Status', { logEvent('Onboarding V2: Connection Status', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,

View File

@ -15,7 +15,6 @@ import ROUTES from 'constants/routes';
import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig'; import { stepsMap } from 'container/OnboardingContainer/constants/stepsConfig';
import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource'; import { DataSourceType } from 'container/OnboardingContainer/Steps/DataSource/DataSource';
import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils'; import { hasFrameworks } from 'container/OnboardingContainer/utils/dataSourceUtils';
import useAnalytics from 'hooks/analytics/useAnalytics';
import history from 'lib/history'; import history from 'lib/history';
import { isEmpty, isNull } from 'lodash-es'; import { isEmpty, isNull } from 'lodash-es';
import { HelpCircle, UserPlus } from 'lucide-react'; import { HelpCircle, UserPlus } from 'lucide-react';
@ -79,7 +78,6 @@ export default function ModuleStepsContainer({
} = useOnboardingContext(); } = useOnboardingContext();
const [current, setCurrent] = useState(0); const [current, setCurrent] = useState(0);
const { trackEvent } = useAnalytics();
const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData); const [metaData, setMetaData] = useState<MetaDataProps[]>(defaultMetaData);
const lastStepIndex = selectedModuleSteps.length - 1; const lastStepIndex = selectedModuleSteps.length - 1;
@ -143,7 +141,7 @@ export default function ModuleStepsContainer({
}; };
const redirectToModules = (): void => { const redirectToModules = (): void => {
trackEvent('Onboarding V2 Complete', { logEvent('Onboarding V2 Complete', {
module: selectedModule.id, module: selectedModule.id,
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
framework: selectedFramework, framework: selectedFramework,
@ -186,14 +184,14 @@ export default function ModuleStepsContainer({
// on next step click track events // on next step click track events
switch (selectedModuleSteps[current].id) { switch (selectedModuleSteps[current].id) {
case stepsMap.dataSource: case stepsMap.dataSource:
trackEvent('Onboarding V2: Data Source Selected', { logEvent('Onboarding V2: Data Source Selected', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
framework: selectedFramework, framework: selectedFramework,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.environmentDetails: case stepsMap.environmentDetails:
trackEvent('Onboarding V2: Environment Selected', { logEvent('Onboarding V2: Environment Selected', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
framework: selectedFramework, framework: selectedFramework,
environment: selectedEnvironment, environment: selectedEnvironment,
@ -201,7 +199,7 @@ export default function ModuleStepsContainer({
}); });
break; break;
case stepsMap.selectMethod: case stepsMap.selectMethod:
trackEvent('Onboarding V2: Method Selected', { logEvent('Onboarding V2: Method Selected', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
framework: selectedFramework, framework: selectedFramework,
environment: selectedEnvironment, environment: selectedEnvironment,
@ -211,7 +209,7 @@ export default function ModuleStepsContainer({
break; break;
case stepsMap.setupOtelCollector: case stepsMap.setupOtelCollector:
trackEvent('Onboarding V2: Setup Otel Collector', { logEvent('Onboarding V2: Setup Otel Collector', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
framework: selectedFramework, framework: selectedFramework,
environment: selectedEnvironment, environment: selectedEnvironment,
@ -220,7 +218,7 @@ export default function ModuleStepsContainer({
}); });
break; break;
case stepsMap.instrumentApplication: case stepsMap.instrumentApplication:
trackEvent('Onboarding V2: Instrument Application', { logEvent('Onboarding V2: Instrument Application', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
framework: selectedFramework, framework: selectedFramework,
environment: selectedEnvironment, environment: selectedEnvironment,
@ -229,13 +227,13 @@ export default function ModuleStepsContainer({
}); });
break; break;
case stepsMap.cloneRepository: case stepsMap.cloneRepository:
trackEvent('Onboarding V2: Clone Repository', { logEvent('Onboarding V2: Clone Repository', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.runApplication: case stepsMap.runApplication:
trackEvent('Onboarding V2: Run Application', { logEvent('Onboarding V2: Run Application', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
framework: selectedFramework, framework: selectedFramework,
environment: selectedEnvironment, environment: selectedEnvironment,
@ -244,95 +242,95 @@ export default function ModuleStepsContainer({
}); });
break; break;
case stepsMap.addHttpDrain: case stepsMap.addHttpDrain:
trackEvent('Onboarding V2: Add HTTP Drain', { logEvent('Onboarding V2: Add HTTP Drain', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.startContainer: case stepsMap.startContainer:
trackEvent('Onboarding V2: Start Container', { logEvent('Onboarding V2: Start Container', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.setupLogDrains: case stepsMap.setupLogDrains:
trackEvent('Onboarding V2: Setup Log Drains', { logEvent('Onboarding V2: Setup Log Drains', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.configureReceiver: case stepsMap.configureReceiver:
trackEvent('Onboarding V2: Configure Receiver', { logEvent('Onboarding V2: Configure Receiver', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.configureAws: case stepsMap.configureAws:
trackEvent('Onboarding V2: Configure AWS', { logEvent('Onboarding V2: Configure AWS', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.sendLogsCloudwatch: case stepsMap.sendLogsCloudwatch:
trackEvent('Onboarding V2: Send Logs Cloudwatch', { logEvent('Onboarding V2: Send Logs Cloudwatch', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.setupDaemonService: case stepsMap.setupDaemonService:
trackEvent('Onboarding V2: Setup ECS Daemon Service', { logEvent('Onboarding V2: Setup ECS Daemon Service', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.createOtelConfig: case stepsMap.createOtelConfig:
trackEvent('Onboarding V2: Create ECS OTel Config', { logEvent('Onboarding V2: Create ECS OTel Config', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.createDaemonService: case stepsMap.createDaemonService:
trackEvent('Onboarding V2: Create ECS Daemon Service', { logEvent('Onboarding V2: Create ECS Daemon Service', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.ecsSendData: case stepsMap.ecsSendData:
trackEvent('Onboarding V2: ECS send traces data', { logEvent('Onboarding V2: ECS send traces data', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.createSidecarCollectorContainer: case stepsMap.createSidecarCollectorContainer:
trackEvent('Onboarding V2: ECS create Sidecar Container', { logEvent('Onboarding V2: ECS create Sidecar Container', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.deployTaskDefinition: case stepsMap.deployTaskDefinition:
trackEvent('Onboarding V2: ECS deploy task definition', { logEvent('Onboarding V2: ECS deploy task definition', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.ecsSendLogsData: case stepsMap.ecsSendLogsData:
trackEvent('Onboarding V2: ECS Fargate send logs data', { logEvent('Onboarding V2: ECS Fargate send logs data', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,
}); });
break; break;
case stepsMap.monitorDashboard: case stepsMap.monitorDashboard:
trackEvent('Onboarding V2: EKS monitor dashboard', { logEvent('Onboarding V2: EKS monitor dashboard', {
dataSource: selectedDataSource?.id, dataSource: selectedDataSource?.id,
environment: selectedEnvironment, environment: selectedEnvironment,
module: activeStep?.module?.id, module: activeStep?.module?.id,

View File

@ -0,0 +1,31 @@
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/dashboard/getAll';
import TablePanelWrapper from '../TablePanelWrapper';
import {
tablePanelQueryResponse,
tablePanelWidgetQuery,
} from './tablePanelWrapperHelper';
describe('Table panel wrappper tests', () => {
it('table should render fine with the query response and column units', () => {
const { container, getByText } = render(
<TablePanelWrapper
widget={(tablePanelWidgetQuery as unknown) as Widgets}
queryResponse={(tablePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}
/>,
);
// checking the overall rendering of the table
expect(container).toMatchSnapshot();
// the first row of the table should have the latency value with units
expect(getByText('4.35 s')).toBeInTheDocument();
// the rows should have optimised value for human readability
expect(getByText('31.3 ms')).toBeInTheDocument();
// the applied legend should appear as the column header
expect(getByText('latency-per-service')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,38 @@
import { render } from 'tests/test-utils';
import { Widgets } from 'types/api/dashboard/getAll';
import ValuePanelWrapper from '../ValuePanelWrapper';
import {
thresholds,
valuePanelQueryResponse,
valuePanelWidget,
} from './valuePanelWrapperHelper';
describe('Value panel wrappper tests', () => {
it('should render value panel correctly with yaxis unit', () => {
const { getByText } = render(
<ValuePanelWrapper
widget={(valuePanelWidget as unknown) as Widgets}
queryResponse={(valuePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}
/>,
);
// selected y axis unit as miliseconds (ms)
expect(getByText('295 ms')).toBeInTheDocument();
});
it('should render tooltip when there are conflicting thresholds', () => {
const { getByTestId, container } = render(
<ValuePanelWrapper
widget={({ ...valuePanelWidget, thresholds } as unknown) as Widgets}
queryResponse={(valuePanelQueryResponse as unknown) as any}
onDragSelect={(): void => {}}
/>,
);
expect(getByTestId('conflicting-thresholds')).toBeInTheDocument();
// added snapshot test here for checking the thresholds color being applied properly
expect(container).toMatchSnapshot();
});
});

View File

@ -0,0 +1,389 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Table panel wrappper tests table should render fine with the query response and column units 1`] = `
.c1 {
position: absolute;
right: -0.313rem;
bottom: 0;
z-index: 1;
width: 0.625rem;
height: 100%;
cursor: col-resize;
}
.c0 {
height: 95%;
overflow: hidden;
}
.c0 .ant-table-wrapper {
height: 100%;
}
.c0 .ant-spin-nested-loading {
height: 100%;
}
.c0 .ant-spin-container {
height: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c0 .ant-table {
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
overflow: auto;
}
.c0 .ant-table > .ant-table-container > .ant-table-content > table {
min-width: 99% !important;
}
<div>
<div
class="c0"
>
<div
class="query-table"
>
<div
class="ant-table-wrapper css-dev-only-do-not-override-2i2tap"
>
<div
class="ant-spin-nested-loading css-dev-only-do-not-override-2i2tap"
>
<div
class="ant-spin-container"
>
<div
class="ant-table ant-table-small ant-table-layout-fixed ant-table-scroll-horizontal"
>
<div
class="ant-table-container"
>
<div
class="ant-table-content"
style="overflow-x: auto; overflow-y: hidden;"
>
<table
style="width: auto; min-width: 100%; table-layout: fixed;"
>
<colgroup>
<col
style="width: 145px;"
/>
<col
style="width: 145px;"
/>
</colgroup>
<thead
class="ant-table-thead"
>
<tr>
<th
aria-label="service_name"
class="ant-table-cell ant-table-column-has-sorters react-resizable"
scope="col"
tabindex="0"
>
<div
class="ant-table-column-sorters"
>
<span
class="ant-table-column-title"
>
service_name
</span>
<span
class="ant-table-column-sorter ant-table-column-sorter-full"
>
<span
aria-hidden="true"
class="ant-table-column-sorter-inner"
>
<span
aria-label="caret-up"
class="anticon anticon-caret-up ant-table-column-sorter-up"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
/>
</svg>
</span>
<span
aria-label="caret-down"
class="anticon anticon-caret-down ant-table-column-sorter-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
/>
</svg>
</span>
</span>
</span>
</div>
<span
class="c1 react-resizable-handle"
/>
</th>
<th
aria-label="latency-per-service"
class="ant-table-cell ant-table-column-has-sorters react-resizable"
scope="col"
tabindex="0"
>
<div
class="ant-table-column-sorters"
>
<span
class="ant-table-column-title"
>
latency-per-service
</span>
<span
class="ant-table-column-sorter ant-table-column-sorter-full"
>
<span
aria-hidden="true"
class="ant-table-column-sorter-inner"
>
<span
aria-label="caret-up"
class="anticon anticon-caret-up ant-table-column-sorter-up"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-up"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
/>
</svg>
</span>
<span
aria-label="caret-down"
class="anticon anticon-caret-down ant-table-column-sorter-down"
role="img"
>
<svg
aria-hidden="true"
data-icon="caret-down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
/>
</svg>
</span>
</span>
</span>
</div>
<span
class="c1 react-resizable-handle"
/>
</th>
</tr>
</thead>
<tbody
class="ant-table-tbody"
>
<tr
aria-hidden="true"
class="ant-table-measure-row"
style="height: 0px; font-size: 0px;"
>
<td
style="padding: 0px; border: 0px; height: 0px;"
>
<div
style="height: 0px; overflow: hidden;"
>
 
</div>
</td>
<td
style="padding: 0px; border: 0px; height: 0px;"
>
<div
style="height: 0px; overflow: hidden;"
>
 
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
demo-app
</div>
</td>
<td
class="ant-table-cell"
>
<div>
4.35 s
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
customer
</div>
</td>
<td
class="ant-table-cell"
>
<div>
431 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
mysql
</div>
</td>
<td
class="ant-table-cell"
>
<div>
431 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
frontend
</div>
</td>
<td
class="ant-table-cell"
>
<div>
287 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
driver
</div>
</td>
<td
class="ant-table-cell"
>
<div>
230 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
route
</div>
</td>
<td
class="ant-table-cell"
>
<div>
66.4 ms
</div>
</td>
</tr>
<tr
class="ant-table-row ant-table-row-level-0"
>
<td
class="ant-table-cell"
>
<div>
redis
</div>
</td>
<td
class="ant-table-cell"
>
<div>
31.3 ms
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,76 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Value panel wrappper tests should render tooltip when there are conflicting thresholds 1`] = `
.c1 {
height: 100%;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
.c0 {
text-align: center;
padding-top: 1rem;
}
<div>
<div
class="c0"
>
<article
class="ant-typography css-dev-only-do-not-override-2i2tap"
/>
</div>
<div
class="c1"
>
<div
class="value-graph-container"
>
<span
class="ant-typography value-graph-text css-dev-only-do-not-override-2i2tap"
style="color: Blue;"
>
295 ms
</span>
<div
class="value-graph-textconflict"
>
<span
aria-label="exclamation-circle"
class="anticon anticon-exclamation-circle value-graph-icon"
data-testid="conflicting-thresholds"
role="img"
>
<svg
aria-hidden="true"
data-icon="exclamation-circle"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm-32 232c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V296zm32 440a48.01 48.01 0 010-96 48.01 48.01 0 010 96z"
/>
</svg>
</span>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,286 @@
export const tablePanelWidgetQuery = {
id: '727533b0-7718-4f99-a1db-a1875649325c',
title: '',
description: '',
isStacked: false,
nullZeroValues: 'zero',
opacity: '1',
panelTypes: 'table',
query: {
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency',
dataType: 'float64',
type: 'ExponentialHistogram',
isColumn: true,
isJSON: false,
id: 'signoz_latency--float64--ExponentialHistogram--true',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [
{
key: 'service_name',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
id: 'service_name--string--tag--false',
},
],
legend: 'latency-per-service',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
id: '7feafec2-a450-4b5a-8897-260c1a9fe1e4',
queryType: 'builder',
},
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,
selectedLogFields: [
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
],
selectedTracesFields: [
{
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
},
{
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
},
],
yAxisUnit: 'none',
thresholds: [],
fillSpans: false,
columnUnits: {
A: 'ms',
},
bucketCount: 30,
stackedBarChart: false,
bucketWidth: 0,
mergeAllActiveQueries: false,
};
export const tablePanelQueryResponse = {
status: 'success',
isLoading: false,
isSuccess: true,
isError: false,
isIdle: false,
data: {
statusCode: 200,
error: null,
message: 'success',
payload: {
status: 'success',
data: {
resultType: '',
result: [
{
table: {
columns: [
{
name: 'service_name',
queryName: '',
isValueColumn: false,
},
{
name: 'A',
queryName: 'A',
isValueColumn: true,
},
],
rows: [
{
data: {
A: 4353.81,
service_name: 'demo-app',
},
},
{
data: {
A: 431.25,
service_name: 'customer',
},
},
{
data: {
A: 431.25,
service_name: 'mysql',
},
},
{
data: {
A: 287.11,
service_name: 'frontend',
},
},
{
data: {
A: 230.02,
service_name: 'driver',
},
},
{
data: {
A: 66.37,
service_name: 'route',
},
},
{
data: {
A: 31.3,
service_name: 'redis',
},
},
],
},
},
],
},
},
params: {
start: 1721207225000,
end: 1721207525000,
step: 60,
variables: {},
formatForWeb: true,
compositeQuery: {
queryType: 'builder',
panelType: 'table',
fillGaps: false,
builderQueries: {
A: {
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency',
dataType: 'float64',
type: 'ExponentialHistogram',
isColumn: true,
isJSON: false,
id: 'signoz_latency--float64--ExponentialHistogram--true',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [
{
key: 'service_name',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
id: 'service_name--string--tag--false',
},
],
legend: '',
reduceTo: 'avg',
},
},
},
},
},
dataUpdatedAt: 1721207526018,
error: null,
errorUpdatedAt: 0,
failureCount: 0,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isRefetching: false,
isLoadingError: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isStale: true,
};

View File

@ -0,0 +1,267 @@
export const valuePanelWidget = {
id: 'b8b93086-ef01-47bf-9044-1e7abd583be4',
title: 'signoz latency in ms',
description: '',
isStacked: false,
nullZeroValues: 'zero',
opacity: '1',
panelTypes: 'value',
query: {
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency',
dataType: 'float64',
type: 'ExponentialHistogram',
isColumn: true,
isJSON: false,
id: 'signoz_latency--float64--ExponentialHistogram--true',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
id: '3bec289c-49c3-4d7e-98bb-84d47c79909c',
queryType: 'builder',
},
timePreferance: 'GLOBAL_TIME',
softMax: null,
softMin: null,
selectedLogFields: [
{
dataType: 'string',
type: '',
name: 'body',
},
{
dataType: 'string',
type: '',
name: 'timestamp',
},
],
selectedTracesFields: [
{
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'serviceName--string--tag--true',
},
{
key: 'name',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'name--string--tag--true',
},
{
key: 'durationNano',
dataType: 'float64',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'durationNano--float64--tag--true',
},
{
key: 'httpMethod',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'httpMethod--string--tag--true',
},
{
key: 'responseStatusCode',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: 'responseStatusCode--string--tag--true',
},
],
yAxisUnit: 'ms',
thresholds: [],
fillSpans: false,
columnUnits: {},
bucketCount: 30,
stackedBarChart: false,
bucketWidth: 0,
mergeAllActiveQueries: false,
};
export const thresholds = [
{
index: '8eb16a3a-b4f1-47c8-943a-4b1786884583',
isEditEnabled: false,
thresholdColor: 'Blue',
thresholdFormat: 'Text',
thresholdOperator: '>',
thresholdUnit: 'none',
thresholdValue: 100,
keyIndex: 1,
selectedGraph: 'value',
thresholdTableOptions: '',
thresholdLabel: '',
},
{
index: 'eb9c1186-ad7d-42dd-8e7f-3913a321d7cf',
isEditEnabled: false,
thresholdColor: 'Red',
thresholdFormat: 'Text',
thresholdOperator: '>',
thresholdUnit: 'none',
thresholdValue: 0,
keyIndex: 0,
selectedGraph: 'value',
thresholdTableOptions: '',
thresholdLabel: '',
},
];
export const valuePanelQueryResponse = {
status: 'success',
isLoading: false,
isSuccess: true,
isError: false,
isIdle: false,
data: {
statusCode: 200,
error: null,
message: 'success',
payload: {
data: {
result: [
{
metric: {
A: 'A',
},
values: [[0, '295.4299833508185']],
queryName: 'A',
legend: 'A',
},
],
resultType: '',
newResult: {
status: 'success',
data: {
resultType: '',
result: [
{
queryName: 'A',
series: [
{
labels: {
A: 'A',
},
labelsArray: null,
values: [
{
timestamp: 0,
value: '295.4299833508185',
},
],
},
],
},
],
},
},
},
},
params: {
start: 1721203451000,
end: 1721203751000,
step: 60,
variables: {},
formatForWeb: false,
compositeQuery: {
queryType: 'builder',
panelType: 'value',
fillGaps: false,
builderQueries: {
A: {
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency',
dataType: 'float64',
type: 'ExponentialHistogram',
isColumn: true,
isJSON: false,
id: 'signoz_latency--float64--ExponentialHistogram--true',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
},
},
},
},
dataUpdatedAt: 1721203751775,
error: null,
errorUpdatedAt: 0,
failureCount: 0,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isRefetching: false,
isLoadingError: false,
isPlaceholderData: false,
isPreviousData: false,
isRefetchError: false,
isStale: true,
};

View File

@ -1,6 +1,6 @@
import { EditFilled, PlusOutlined } from '@ant-design/icons'; import { EditFilled, PlusOutlined } from '@ant-design/icons';
import logEvent from 'api/common/logEvent';
import TextToolTip from 'components/TextToolTip'; import TextToolTip from 'components/TextToolTip';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ActionMode, ActionType, Pipeline } from 'types/api/pipeline/def'; import { ActionMode, ActionType, Pipeline } from 'types/api/pipeline/def';
@ -15,7 +15,6 @@ function CreatePipelineButton({
pipelineData, pipelineData,
}: CreatePipelineButtonProps): JSX.Element { }: CreatePipelineButtonProps): JSX.Element {
const { t } = useTranslation(['pipeline']); const { t } = useTranslation(['pipeline']);
const { trackEvent } = useAnalytics();
const isAddNewPipelineVisible = useMemo( const isAddNewPipelineVisible = useMemo(
() => checkDataLength(pipelineData?.pipelines), () => checkDataLength(pipelineData?.pipelines),
@ -26,7 +25,7 @@ function CreatePipelineButton({
const onEnterEditMode = (): void => { const onEnterEditMode = (): void => {
setActionMode(ActionMode.Editing); setActionMode(ActionMode.Editing);
trackEvent('Logs: Pipelines: Entered Edit Mode', { logEvent('Logs: Pipelines: Entered Edit Mode', {
source: 'signoz-ui', source: 'signoz-ui',
}); });
}; };
@ -34,7 +33,7 @@ function CreatePipelineButton({
setActionMode(ActionMode.Editing); setActionMode(ActionMode.Editing);
setActionType(ActionType.AddPipeline); setActionType(ActionType.AddPipeline);
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', { logEvent('Logs: Pipelines: Clicked Add New Pipeline', {
source: 'signoz-ui', source: 'signoz-ui',
}); });
}; };

View File

@ -1,6 +1,6 @@
import { PlusCircleOutlined } from '@ant-design/icons'; import { PlusCircleOutlined } from '@ant-design/icons';
import { TableLocale } from 'antd/es/table/interface'; import { TableLocale } from 'antd/es/table/interface';
import useAnalytics from 'hooks/analytics/useAnalytics'; import logEvent from 'api/common/logEvent';
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
@ -39,7 +39,6 @@ function PipelineExpandView({
}: PipelineExpandViewProps): JSX.Element { }: PipelineExpandViewProps): JSX.Element {
const { t } = useTranslation(['pipeline']); const { t } = useTranslation(['pipeline']);
const isDarkMode = useIsDarkMode(); const isDarkMode = useIsDarkMode();
const { trackEvent } = useAnalytics();
const isEditingActionMode = isActionMode === ActionMode.Editing; const isEditingActionMode = isActionMode === ActionMode.Editing;
const deleteProcessorHandler = useCallback( const deleteProcessorHandler = useCallback(
@ -192,7 +191,7 @@ function PipelineExpandView({
const addNewProcessorHandler = useCallback((): void => { const addNewProcessorHandler = useCallback((): void => {
setActionType(ActionType.AddProcessor); setActionType(ActionType.AddProcessor);
trackEvent('Logs: Pipelines: Clicked Add New Processor', { logEvent('Logs: Pipelines: Clicked Add New Processor', {
source: 'signoz-ui', source: 'signoz-ui',
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -5,7 +5,6 @@ import { Card, Modal, Table, Typography } from 'antd';
import { ExpandableConfig } from 'antd/es/table/interface'; import { ExpandableConfig } from 'antd/es/table/interface';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import savePipeline from 'api/pipeline/post'; import savePipeline from 'api/pipeline/post';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { isUndefined } from 'lodash-es'; import { isUndefined } from 'lodash-es';
import cloneDeep from 'lodash-es/cloneDeep'; import cloneDeep from 'lodash-es/cloneDeep';
@ -100,7 +99,6 @@ function PipelineListsView({
const [modal, contextHolder] = Modal.useModal(); const [modal, contextHolder] = Modal.useModal();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const [pipelineSearchValue, setPipelineSearchValue] = useState<string>(''); const [pipelineSearchValue, setPipelineSearchValue] = useState<string>('');
const { trackEvent } = useAnalytics();
const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>( const [prevPipelineData, setPrevPipelineData] = useState<Array<PipelineData>>(
cloneDeep(pipelineData?.pipelines || []), cloneDeep(pipelineData?.pipelines || []),
); );
@ -376,7 +374,7 @@ function PipelineListsView({
const addNewPipelineHandler = useCallback((): void => { const addNewPipelineHandler = useCallback((): void => {
setActionType(ActionType.AddPipeline); setActionType(ActionType.AddPipeline);
trackEvent('Logs: Pipelines: Clicked Add New Pipeline', { logEvent('Logs: Pipelines: Clicked Add New Pipeline', {
source: 'signoz-ui', source: 'signoz-ui',
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@ -415,7 +413,7 @@ function PipelineListsView({
setCurrPipelineData(pipelinesInDB); setCurrPipelineData(pipelinesInDB);
setPrevPipelineData(pipelinesInDB); setPrevPipelineData(pipelinesInDB);
trackEvent('Logs: Pipelines: Saved Pipelines', { logEvent('Logs: Pipelines: Saved Pipelines', {
count: pipelinesInDB.length, count: pipelinesInDB.length,
enabled: pipelinesInDB.filter((p) => p.enabled).length, enabled: pipelinesInDB.filter((p) => p.enabled).length,
source: 'signoz-ui', source: 'signoz-ui',

View File

@ -1,15 +1,13 @@
import { EyeFilled } from '@ant-design/icons'; import { EyeFilled } from '@ant-design/icons';
import { Divider, Modal } from 'antd'; import { Divider, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import PipelineProcessingPreview from 'container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview'; import PipelineProcessingPreview from 'container/PipelinePage/PipelineListsView/Preview/PipelineProcessingPreview';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useState } from 'react'; import { useState } from 'react';
import { PipelineData } from 'types/api/pipeline/def'; import { PipelineData } from 'types/api/pipeline/def';
import { iconStyle } from '../../../config'; import { iconStyle } from '../../../config';
function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null { function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null {
const { trackEvent } = useAnalytics();
const [previewKey, setPreviewKey] = useState<string | null>(null); const [previewKey, setPreviewKey] = useState<string | null>(null);
const isModalOpen = Boolean(previewKey); const isModalOpen = Boolean(previewKey);
@ -23,7 +21,7 @@ function PreviewAction({ pipeline }: PreviewActionProps): JSX.Element | null {
const onOpenPreview = (): void => { const onOpenPreview = (): void => {
openModal(); openModal();
trackEvent('Logs: Pipelines: Clicked Preview Pipeline', { logEvent('Logs: Pipelines: Clicked Preview Pipeline', {
source: 'signoz-ui', source: 'signoz-ui',
}); });
}; };

View File

@ -1,5 +1,6 @@
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import logEvent from 'api/common/logEvent';
import { I18nextProvider } from 'react-i18next'; import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
@ -9,14 +10,7 @@ import store from 'store';
import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton'; import CreatePipelineButton from '../Layouts/Pipeline/CreatePipelineButton';
import { pipelineApiResponseMockData } from '../mocks/pipeline'; import { pipelineApiResponseMockData } from '../mocks/pipeline';
const trackEventVar = jest.fn(); jest.mock('api/common/logEvent');
jest.mock('hooks/analytics/useAnalytics', () => ({
__esModule: true,
default: jest.fn().mockImplementation(() => ({
trackEvent: trackEventVar,
trackPageView: jest.fn(),
})),
}));
describe('PipelinePage container test', () => { describe('PipelinePage container test', () => {
it('should render CreatePipelineButton section', async () => { it('should render CreatePipelineButton section', async () => {
@ -58,7 +52,7 @@ describe('PipelinePage container test', () => {
expect(editButton).toBeInTheDocument(); expect(editButton).toBeInTheDocument();
await userEvent.click(editButton); await userEvent.click(editButton);
expect(trackEventVar).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', { expect(logEvent).toBeCalledWith('Logs: Pipelines: Entered Edit Mode', {
source: 'signoz-ui', source: 'signoz-ui',
}); });
}); });
@ -83,11 +77,8 @@ describe('PipelinePage container test', () => {
expect(editButton).toBeInTheDocument(); expect(editButton).toBeInTheDocument();
await userEvent.click(editButton); await userEvent.click(editButton);
expect(trackEventVar).toBeCalledWith( expect(logEvent).toBeCalledWith('Logs: Pipelines: Clicked Add New Pipeline', {
'Logs: Pipelines: Clicked Add New Pipeline', source: 'signoz-ui',
{ });
source: 'signoz-ui',
},
);
}); });
}); });

View File

@ -59,7 +59,7 @@ function ServiceTraces(): JSX.Element {
logEvent('APM: List page visited', { logEvent('APM: List page visited', {
numberOfServices: data?.length, numberOfServices: data?.length,
selectedEnvironments, selectedEnvironments,
resourceAttributeUsed: !!queries.length, resourceAttributeUsed: !!queries?.length,
rps, rps,
}); });
logEventCalledRef.current = true; logEventCalledRef.current = true;

View File

@ -324,8 +324,8 @@ function SideNav({
onClickHandler(item?.key as string, event); onClickHandler(item?.key as string, event);
} }
logEvent('Sidebar: Menu clicked', { logEvent('Sidebar: Menu clicked', {
menuRoute: item.key, menuRoute: item?.key,
menuLabel: item.label, menuLabel: item?.label,
}); });
}; };
@ -455,8 +455,8 @@ function SideNav({
onClick={(event: MouseEvent): void => { onClick={(event: MouseEvent): void => {
handleUserManagentMenuItemClick(item?.key as string, event); handleUserManagentMenuItemClick(item?.key as string, event);
logEvent('Sidebar: Menu clicked', { logEvent('Sidebar: Menu clicked', {
menuRoute: item.key, menuRoute: item?.key,
menuLabel: item.label, menuLabel: item?.label,
}); });
}} }}
/> />
@ -475,8 +475,8 @@ function SideNav({
history.push(`${inviteMemberMenuItem.key}`); history.push(`${inviteMemberMenuItem.key}`);
} }
logEvent('Sidebar: Menu clicked', { logEvent('Sidebar: Menu clicked', {
menuRoute: inviteMemberMenuItem.key, menuRoute: inviteMemberMenuItem?.key,
menuLabel: inviteMemberMenuItem.label, menuLabel: inviteMemberMenuItem?.label,
}); });
}} }}
/> />
@ -493,7 +493,7 @@ function SideNav({
event, event,
); );
logEvent('Sidebar: Menu clicked', { logEvent('Sidebar: Menu clicked', {
menuRoute: userSettingsMenuItem.key, menuRoute: userSettingsMenuItem?.key,
menuLabel: 'User', menuLabel: 'User',
}); });
}} }}

View File

@ -1,5 +1,4 @@
import { Typography } from 'antd'; import { Card, Typography } from 'antd';
import Card from 'antd/es/card/Card';
import styled from 'styled-components'; import styled from 'styled-components';
export const Container = styled(Card)` export const Container = styled(Card)`

View File

@ -23,7 +23,7 @@ import NewExplorerCTA from 'container/NewExplorerCTA';
import dayjs, { Dayjs } from 'dayjs'; import dayjs, { Dayjs } from 'dayjs';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import GetMinMax from 'lib/getMinMax'; import GetMinMax, { isValidTimeFormat } from 'lib/getMinMax';
import getTimeString from 'lib/getTimeString'; import getTimeString from 'lib/getTimeString';
import history from 'lib/history'; import history from 'lib/history';
import { isObject } from 'lodash-es'; import { isObject } from 'lodash-es';
@ -73,6 +73,7 @@ function DateTimeSelection({
const urlQuery = useUrlQuery(); const urlQuery = useUrlQuery();
const searchStartTime = urlQuery.get('startTime'); const searchStartTime = urlQuery.get('startTime');
const searchEndTime = urlQuery.get('endTime'); const searchEndTime = urlQuery.get('endTime');
const relativeTimeFromUrl = urlQuery.get(QueryParams.relativeTime);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(false); const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(false);
const [isValidteRelativeTime, setIsValidteRelativeTime] = useState(false); const [isValidteRelativeTime, setIsValidteRelativeTime] = useState(false);
@ -404,9 +405,18 @@ function DateTimeSelection({
time: Time, time: Time,
currentRoute: string, currentRoute: string,
): Time | CustomTimeType => { ): Time | CustomTimeType => {
// if the relativeTime param is present in the url give top most preference to the same
// if the relativeTime param is not valid then move to next preference
if (relativeTimeFromUrl != null && isValidTimeFormat(relativeTimeFromUrl)) {
return relativeTimeFromUrl as Time;
}
// if the startTime and endTime params are present in the url give next preference to the them.
if (searchEndTime !== null && searchStartTime !== null) { if (searchEndTime !== null && searchStartTime !== null) {
return 'custom'; return 'custom';
} }
// if nothing is present in the url for time range then rely on the local storage values
if ( if (
(localstorageEndTime === null || localstorageStartTime === null) && (localstorageEndTime === null || localstorageStartTime === null) &&
time === 'custom' time === 'custom'
@ -414,6 +424,7 @@ function DateTimeSelection({
return getDefaultOption(currentRoute); return getDefaultOption(currentRoute);
} }
// if not present in the local storage as well then rely on the defaults set for the page
if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) { if (OLD_RELATIVE_TIME_VALUES.indexOf(time) > -1) {
return convertOldTimeToNewValidCustomTimeFormat(time); return convertOldTimeToNewValidCustomTimeFormat(time);
} }
@ -448,7 +459,11 @@ function DateTimeSelection({
setRefreshButtonHidden(updatedTime === 'custom'); setRefreshButtonHidden(updatedTime === 'custom');
updateTimeInterval(updatedTime, [preStartTime, preEndTime]); if (updatedTime !== 'custom') {
updateTimeInterval(updatedTime);
} else {
updateTimeInterval(updatedTime, [preStartTime, preEndTime]);
}
if (updatedTime !== 'custom') { if (updatedTime !== 'custom') {
urlQuery.delete('startTime'); urlQuery.delete('startTime');

View File

@ -31,7 +31,14 @@ function TopNav(): JSX.Element | null {
[location.pathname], [location.pathname],
); );
if (isSignUpPage || isDisabled || isRouteToSkip) { const isNewAlertsLandingPage = useMemo(
() =>
matchPath(location.pathname, { path: ROUTES.ALERTS_NEW, exact: true }) &&
!location.search,
[location.pathname, location.search],
);
if (isSignUpPage || isDisabled || isRouteToSkip || isNewAlertsLandingPage) {
return null; return null;
} }

View File

@ -1,8 +1,7 @@
import './TraceDetails.styles.scss'; import './TraceDetails.styles.scss';
import { FilterOutlined } from '@ant-design/icons'; import { FilterOutlined } from '@ant-design/icons';
import { Button, Col, Typography } from 'antd'; import { Button, Col, Layout, Typography } from 'antd';
import Sider from 'antd/es/layout/Sider';
import cx from 'classnames'; import cx from 'classnames';
import { import {
StyledCol, StyledCol,
@ -42,6 +41,8 @@ import {
INTERVAL_UNITS, INTERVAL_UNITS,
} from './utils'; } from './utils';
const { Sider } = Layout;
function TraceDetail({ response }: TraceDetailProps): JSX.Element { function TraceDetail({ response }: TraceDetailProps): JSX.Element {
const spanServiceColors = useMemo( const spanServiceColors = useMemo(
() => spanServiceNameToColorMapping(response[0].events), () => spanServiceNameToColorMapping(response[0].events),

View File

@ -1,5 +1,4 @@
import { Col } from 'antd'; import { Card, Col } from 'antd';
import Card from 'antd/es/card/Card';
import styled from 'styled-components'; import styled from 'styled-components';
export const Container = styled(Card)` export const Container = styled(Card)`

View File

@ -32,16 +32,6 @@ const useAnalytics = (): any => {
} }
}; };
// useEffect(() => {
// // Perform any setup or cleanup related to the analytics library
// // For example, initialize analytics library here
// // Clean-up function (optional)
// return () => {
// // Perform cleanup if needed
// };
// }, []); // The empty dependency array ensures that this effect runs only once when the component mounts
return { trackPageView, trackEvent }; return { trackPageView, trackEvent };
}; };

View File

@ -61,7 +61,10 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
}); });
queryRangeMutation.mutate(queryPayload, { queryRangeMutation.mutate(queryPayload, {
onSuccess: (data) => { onSuccess: (data) => {
const updatedQuery = mapQueryDataFromApi(data.compositeQuery); const updatedQuery = mapQueryDataFromApi(
data.compositeQuery,
widget?.query,
);
history.push( history.push(
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent( `${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(

View File

@ -0,0 +1,56 @@
import { mapQueryDataFromApi } from '../mapQueryDataFromApi';
import {
compositeQueriesWithFunctions,
compositeQueryWithoutVariables,
compositeQueryWithVariables,
defaultOutput,
outputWithFunctions,
replaceVariables,
stepIntervalUnchanged,
widgetQueriesWithFunctions,
widgetQueryWithoutVariables,
widgetQueryWithVariables,
} from './mapQueryDataFromApiInputs';
jest.mock('uuid', () => ({
v4: (): string => 'test-id',
}));
describe('mapQueryDataFromApi function tests', () => {
it('should not update the step interval when query is passed', () => {
const output = mapQueryDataFromApi(
compositeQueryWithoutVariables,
widgetQueryWithoutVariables,
);
// composite query is the response from the `v3/query_range/format` API call.
// even if the composite query returns stepInterval updated do not modify it
expect(output).toStrictEqual(stepIntervalUnchanged);
});
it('should update filter from the composite query', () => {
const output = mapQueryDataFromApi(
compositeQueryWithVariables,
widgetQueryWithVariables,
);
// replace the variables in the widget query and leave the rest items untouched
expect(output).toStrictEqual(replaceVariables);
});
it('should not update the step intervals with multiple queries and functions', () => {
const output = mapQueryDataFromApi(
compositeQueriesWithFunctions,
widgetQueriesWithFunctions,
);
expect(output).toStrictEqual(outputWithFunctions);
});
it('should use the default query values and the compositeQuery object when query is not passed', () => {
const output = mapQueryDataFromApi(compositeQueryWithoutVariables);
// when the query object is not passed take the initial values and merge the composite query on top of it
expect(output).toStrictEqual(defaultOutput);
});
});

View File

@ -0,0 +1,741 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
export const compositeQueryWithoutVariables = ({
builderQueries: {
A: {
queryName: 'A',
stepInterval: 240,
dataSource: DataSource.METRICS,
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_operations',
dataType: DataTypes.Float64,
type: 'Sum',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [],
},
expression: 'A',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
reduceTo: 'avg',
timeAggregation: 'rate',
spaceAggregation: 'sum',
ShiftBy: 0,
},
},
panelType: PANEL_TYPES.TIME_SERIES,
queryType: EQueryType.QUERY_BUILDER,
} as unknown) as ICompositeMetricQuery;
export const widgetQueryWithoutVariables = ({
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_operations',
dataType: 'float64',
type: 'Sum',
isColumn: true,
isJSON: false,
id: 'system_disk_operations--float64--Sum--true',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [],
},
id: '2bbbd8d8-db99-40be-b9c6-9e197c5bc537',
queryType: 'builder',
} as unknown) as Query;
export const stepIntervalUnchanged = {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: 'float64',
id: 'system_disk_operations--float64--Sum--true',
isColumn: true,
isJSON: false,
key: 'system_disk_operations',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: 'metrics',
disabled: false,
expression: 'A',
filters: {
items: [],
op: 'AND',
},
functions: [],
groupBy: [],
having: [],
legend: '',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: 'test-id',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: 'builder',
unit: undefined,
};
export const compositeQueryWithVariables = ({
builderQueries: {
A: {
queryName: 'A',
stepInterval: 240,
dataSource: 'metrics',
aggregateOperator: 'sum_rate',
aggregateAttribute: {
key: 'signoz_calls_total',
dataType: 'float64',
type: '',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [
{
key: {
key: 'deployment_environment',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
value: 'default',
op: 'in',
},
{
key: {
key: 'service_name',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
value: 'frontend',
op: 'in',
},
{
key: {
key: 'operation',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
value: 'HTTP GET /dispatch',
op: 'in',
},
],
},
groupBy: [
{
key: 'service_name',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
{
key: 'operation',
dataType: 'string',
type: 'tag',
isColumn: false,
isJSON: false,
},
],
expression: 'A',
disabled: false,
legend: '{{service_name}}-{{operation}}',
limit: 0,
offset: 0,
pageSize: 0,
reduceTo: 'sum',
timeAggregation: 'rate',
spaceAggregation: 'sum',
ShiftBy: 0,
},
},
panelType: 'graph',
queryType: 'builder',
} as unknown) as ICompositeMetricQuery;
export const widgetQueryWithVariables = ({
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'sum_rate',
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_calls_total--float64----true',
isColumn: true,
isJSON: false,
key: 'signoz_calls_total',
type: '',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [
{
id: 'aa56621e',
key: {
dataType: 'string',
id: 'deployment_environment--string--tag--false',
isColumn: false,
isJSON: false,
key: 'deployment_environment',
type: 'tag',
},
op: 'in',
value: ['{{.deployment_environment}}'],
},
{
id: '97055a02',
key: {
dataType: 'string',
id: 'service_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
},
op: 'in',
value: ['{{.service_name}}'],
},
{
id: '8c4599f2',
key: {
dataType: 'string',
id: 'operation--string--tag--false',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
},
op: 'in',
value: ['{{.endpoint}}'],
},
],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 60,
having: [],
limit: null,
orderBy: [],
groupBy: [
{
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
id: 'service_name--string--tag--false',
},
{
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
id: 'operation--string--tag--false',
},
],
legend: '{{service_name}}-{{operation}}',
reduceTo: 'sum',
},
],
queryFormulas: [],
},
id: '64fcd7be-61d0-4f92-bbb2-1449b089f766',
queryType: 'builder',
} as unknown) as Query;
export const replaceVariables = {
builder: {
queryData: [
{
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_calls_total--float64----true',
isColumn: true,
isJSON: false,
key: 'signoz_calls_total',
type: '',
},
aggregateOperator: 'sum_rate',
dataSource: 'metrics',
disabled: false,
expression: 'A',
filters: {
items: [
{
key: {
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'deployment_environment',
type: 'tag',
},
op: 'in',
value: 'default',
},
{
key: {
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
},
op: 'in',
value: 'frontend',
},
{
key: {
dataType: 'string',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
},
op: 'in',
value: 'HTTP GET /dispatch',
},
],
op: 'AND',
},
functions: [],
groupBy: [
{
dataType: 'string',
id: 'service_name--string--tag--false',
isColumn: false,
isJSON: false,
key: 'service_name',
type: 'tag',
},
{
dataType: 'string',
id: 'operation--string--tag--false',
isColumn: false,
isJSON: false,
key: 'operation',
type: 'tag',
},
],
having: [],
legend: '{{service_name}}-{{operation}}',
limit: null,
orderBy: [],
queryName: 'A',
reduceTo: 'sum',
spaceAggregation: 'sum',
stepInterval: 60,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
id: 'test-id',
promql: [
{
disabled: false,
legend: '',
name: 'A',
query: '',
},
],
queryType: 'builder',
unit: undefined,
};
export const defaultOutput = {
builder: {
queryData: [
{
ShiftBy: 0,
aggregateAttribute: {
dataType: 'float64',
isColumn: true,
isJSON: false,
key: 'system_disk_operations',
type: 'Sum',
},
aggregateOperator: 'rate',
dataSource: 'metrics',
disabled: false,
expression: 'A',
filters: { items: [], op: 'AND' },
functions: [],
groupBy: [],
having: [],
legend: '',
limit: 0,
offset: 0,
orderBy: [],
pageSize: 0,
queryName: 'A',
reduceTo: 'avg',
spaceAggregation: 'sum',
stepInterval: 240,
timeAggregation: 'rate',
},
],
queryFormulas: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'test-id',
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
queryType: 'builder',
unit: undefined,
};
export const compositeQueriesWithFunctions = ({
builderQueries: {
A: {
queryName: 'A',
stepInterval: 60,
dataSource: 'metrics',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'signoz_latency_bucket',
dataType: 'float64',
type: 'Histogram',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [],
},
expression: 'A',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
reduceTo: 'avg',
spaceAggregation: 'p90',
ShiftBy: 0,
},
B: {
queryName: 'B',
stepInterval: 120,
dataSource: 'metrics',
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_io',
dataType: 'float64',
type: 'Sum',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [],
},
expression: 'B',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
reduceTo: 'avg',
timeAggregation: 'rate',
spaceAggregation: 'sum',
ShiftBy: 0,
},
F1: {
queryName: 'F1',
stepInterval: 1,
dataSource: '',
aggregateOperator: '',
aggregateAttribute: {
key: '',
dataType: '',
type: '',
isColumn: false,
isJSON: false,
},
expression: 'A / B ',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
ShiftBy: 0,
},
},
panelType: 'graph',
queryType: 'builder',
} as unknown) as ICompositeMetricQuery;
export const widgetQueriesWithFunctions = ({
clickhouse_sql: [
{
name: 'A',
legend: '',
disabled: false,
query: '',
},
],
promql: [
{
name: 'A',
query: '',
legend: '',
disabled: false,
},
],
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_latency_bucket--float64--Histogram--true',
isColumn: true,
isJSON: false,
key: 'signoz_latency_bucket',
type: 'Histogram',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'A',
disabled: false,
stepInterval: 120,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
{
dataSource: 'metrics',
queryName: 'B',
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_io',
dataType: 'float64',
type: 'Sum',
isColumn: true,
isJSON: false,
id: 'system_disk_io--float64--Sum--true',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
items: [],
op: 'AND',
},
expression: 'B',
disabled: false,
stepInterval: 120,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [
{
queryName: 'F1',
expression: 'A / B ',
disabled: false,
legend: '',
},
],
},
id: '5d1844fe-9b44-4f15-b6fe-f1b843550b77',
queryType: 'builder',
} as unknown) as Query;
export const outputWithFunctions = {
builder: {
queryData: [
{
dataSource: 'metrics',
queryName: 'A',
aggregateOperator: 'count',
aggregateAttribute: {
dataType: 'float64',
id: 'signoz_latency_bucket--float64--Histogram--true',
isColumn: true,
isJSON: false,
key: 'signoz_latency_bucket',
type: 'Histogram',
},
timeAggregation: '',
spaceAggregation: 'p90',
functions: [],
filters: {
op: 'AND',
items: [],
},
expression: 'A',
disabled: false,
stepInterval: 120,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
{
dataSource: 'metrics',
queryName: 'B',
aggregateOperator: 'rate',
aggregateAttribute: {
key: 'system_disk_io',
dataType: 'float64',
type: 'Sum',
isColumn: true,
isJSON: false,
id: 'system_disk_io--float64--Sum--true',
},
timeAggregation: 'rate',
spaceAggregation: 'sum',
functions: [],
filters: {
op: 'AND',
items: [],
},
expression: 'B',
disabled: false,
stepInterval: 120,
having: [],
limit: null,
orderBy: [],
groupBy: [],
legend: '',
reduceTo: 'avg',
},
],
queryFormulas: [
{
queryName: 'F1',
expression: 'A / B ',
disabled: false,
legend: '',
},
],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'test-id',
promql: [{ disabled: false, legend: '', name: 'A', query: '' }],
queryType: 'builder',
unit: undefined,
};

View File

@ -7,9 +7,13 @@ import { transformQueryBuilderDataModel } from '../transformQueryBuilderDataMode
export const mapQueryDataFromApi = ( export const mapQueryDataFromApi = (
compositeQuery: ICompositeMetricQuery, compositeQuery: ICompositeMetricQuery,
query?: Query,
): Query => { ): Query => {
const builder = compositeQuery.builderQueries const builder = compositeQuery.builderQueries
? transformQueryBuilderDataModel(compositeQuery.builderQueries) ? transformQueryBuilderDataModel(
compositeQuery.builderQueries,
query?.builder,
)
: initialQueryState.builder; : initialQueryState.builder;
const promql = compositeQuery.promQueries const promql = compositeQuery.promQueries

View File

@ -3,6 +3,7 @@ import {
initialQueryBuilderFormValuesMap, initialQueryBuilderFormValuesMap,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import { FORMULA_REGEXP } from 'constants/regExp'; import { FORMULA_REGEXP } from 'constants/regExp';
import { isUndefined } from 'lodash-es';
import { import {
BuilderQueryDataResourse, BuilderQueryDataResourse,
IBuilderFormula, IBuilderFormula,
@ -12,6 +13,7 @@ import { QueryBuilderData } from 'types/common/queryBuilder';
export const transformQueryBuilderDataModel = ( export const transformQueryBuilderDataModel = (
data: BuilderQueryDataResourse, data: BuilderQueryDataResourse,
query?: QueryBuilderData,
): QueryBuilderData => { ): QueryBuilderData => {
const queryData: QueryBuilderData['queryData'] = []; const queryData: QueryBuilderData['queryData'] = [];
const queryFormulas: QueryBuilderData['queryFormulas'] = []; const queryFormulas: QueryBuilderData['queryFormulas'] = [];
@ -19,10 +21,37 @@ export const transformQueryBuilderDataModel = (
Object.entries(data).forEach(([, value]) => { Object.entries(data).forEach(([, value]) => {
if (FORMULA_REGEXP.test(value.queryName)) { if (FORMULA_REGEXP.test(value.queryName)) {
const formula = value as IBuilderFormula; const formula = value as IBuilderFormula;
queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula }); const baseFormula = query?.queryFormulas?.find(
(f) => f.queryName === value.queryName,
);
if (!isUndefined(baseFormula)) {
// this is part of the flow where we create alerts from dashboard.
// we pass the formula as is from the widget query as we do not want anything to update in formula from the format api call
queryFormulas.push({ ...baseFormula });
} else {
queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula });
}
} else { } else {
const query = value as IBuilderQuery; const queryFromData = value as IBuilderQuery;
queryData.push({ ...initialQueryBuilderFormValuesMap.metrics, ...query }); const baseQuery = query?.queryData?.find(
(q) => q.queryName === queryFromData.queryName,
);
if (!isUndefined(baseQuery)) {
// this is part of the flow where we create alerts from dashboard.
// we pass the widget query as the base query and accept the filters from the format API response.
// which fills the variable values inside the same and is used to create alerts
// do not accept the full object as the stepInterval field is subject to changes
queryData.push({
...baseQuery,
filters: queryFromData.filters,
});
} else {
queryData.push({
...initialQueryBuilderFormValuesMap.metrics,
...queryFromData,
});
}
} }
}); });

View File

@ -0,0 +1,101 @@
/* eslint-disable sonarjs/no-duplicate-string */
export const dashboardSuccessResponse = {
status: 'success',
data: [
{
id: 1,
uuid: '1',
created_at: '2022-11-16T13:29:47.064874419Z',
created_by: null,
updated_at: '2024-05-21T06:41:30.546630961Z',
updated_by: 'thor@avengers.io',
isLocked: 0,
data: {
collapsableRowsMigrated: true,
description: '',
name: '',
panelMap: {},
tags: ['linux'],
title: 'thor',
uploadedGrafana: false,
uuid: '',
version: '',
},
},
{
id: 2,
uuid: '2',
created_at: '2022-11-16T13:20:47.064874419Z',
created_by: null,
updated_at: '2024-05-21T06:42:30.546630961Z',
updated_by: 'captain-america@avengers.io',
isLocked: 0,
data: {
collapsableRowsMigrated: true,
description: '',
name: '',
panelMap: {},
tags: ['linux'],
title: 'captain america',
uploadedGrafana: false,
uuid: '',
version: '',
},
},
],
};
export const dashboardEmptyState = {
status: 'sucsess',
data: [],
};
export const getDashboardById = {
status: 'success',
data: {
id: 1,
uuid: '1',
created_at: '2022-11-16T13:29:47.064874419Z',
created_by: 'integration',
updated_at: '2024-05-21T06:41:30.546630961Z',
updated_by: 'thor@avengers.io',
isLocked: true,
data: {
collapsableRowsMigrated: true,
description: '',
name: '',
panelMap: {},
tags: ['linux'],
title: 'thor',
uploadedGrafana: false,
uuid: '',
version: '',
variables: {},
},
},
};
export const getNonIntegrationDashboardById = {
status: 'success',
data: {
id: 1,
uuid: '1',
created_at: '2022-11-16T13:29:47.064874419Z',
created_by: 'thor',
updated_at: '2024-05-21T06:41:30.546630961Z',
updated_by: 'thor@avengers.io',
isLocked: true,
data: {
collapsableRowsMigrated: true,
description: '',
name: '',
panelMap: {},
tags: ['linux'],
title: 'thor',
uploadedGrafana: false,
uuid: '',
version: '',
variables: {},
},
},
};

View File

@ -0,0 +1,81 @@
export const explorerView = {
status: 'success',
data: [
{
uuid: 'test-uuid-1',
name: 'Table View',
category: '',
createdAt: '2023-08-29T18:04:10.906310033Z',
createdBy: 'test-user-1',
updatedAt: '2024-01-29T10:42:47.346331133Z',
updatedBy: 'test-user-1',
sourcePage: 'traces',
tags: [''],
compositeQuery: {
builderQueries: {
A: {
queryName: 'A',
stepInterval: 60,
dataSource: 'traces',
aggregateOperator: 'count',
aggregateAttribute: {
key: 'component',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
},
filters: {
op: 'AND',
items: [
{
key: {
key: 'component',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
},
value: 'test-component',
op: '!=',
},
],
},
groupBy: [
{
key: 'component',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
},
{
key: 'client-uuid',
dataType: 'string',
type: 'resource',
isColumn: false,
isJSON: false,
},
],
expression: 'A',
disabled: false,
limit: 0,
offset: 0,
pageSize: 0,
orderBy: [
{
columnName: 'timestamp',
order: 'desc',
},
],
reduceTo: 'sum',
ShiftBy: 0,
},
},
panelType: 'table',
queryType: 'builder',
},
extraData: '{"color":"#00ffd0"}',
},
],
};

View File

@ -1,6 +1,11 @@
import { rest } from 'msw'; import { rest } from 'msw';
import { billingSuccessResponse } from './__mockdata__/billing'; import { billingSuccessResponse } from './__mockdata__/billing';
import {
dashboardSuccessResponse,
getDashboardById,
} from './__mockdata__/dashboards';
import { explorerView } from './__mockdata__/explorer_views';
import { inviteUser } from './__mockdata__/invite_user'; import { inviteUser } from './__mockdata__/invite_user';
import { licensesSuccessResponse } from './__mockdata__/licenses'; import { licensesSuccessResponse } from './__mockdata__/licenses';
import { membersResponse } from './__mockdata__/members'; import { membersResponse } from './__mockdata__/members';
@ -54,6 +59,51 @@ export const handlers = [
const metricName = req.url.searchParams.get('metricName'); const metricName = req.url.searchParams.get('metricName');
const tagKey = req.url.searchParams.get('tagKey'); const tagKey = req.url.searchParams.get('tagKey');
const attributeKey = req.url.searchParams.get('attributeKey');
if (attributeKey === 'serviceName') {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
stringAttributeValues: [
'customer',
'demo-app',
'driver',
'frontend',
'mysql',
'redis',
'route',
'go-grpc-otel-server',
'test',
],
numberAttributeValues: null,
boolAttributeValues: null,
},
}),
);
}
if (attributeKey === 'name') {
return res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
stringAttributeValues: [
'HTTP GET',
'HTTP GET /customer',
'HTTP GET /dispatch',
'HTTP GET /route',
],
numberAttributeValues: null,
boolAttributeValues: null,
},
}),
);
}
if ( if (
metricName === 'signoz_calls_total' && metricName === 'signoz_calls_total' &&
tagKey === 'resource_signoz_collector_id' tagKey === 'resource_signoz_collector_id'
@ -86,15 +136,49 @@ export const handlers = [
res(ctx.status(200), ctx.json(licensesSuccessResponse)), res(ctx.status(200), ctx.json(licensesSuccessResponse)),
), ),
// ?licenseKey=58707e3d-3bdb-44e7-8c89-a9be237939f4
rest.get('http://localhost/api/v1/billing', (req, res, ctx) => rest.get('http://localhost/api/v1/billing', (req, res, ctx) =>
res(ctx.status(200), ctx.json(billingSuccessResponse)), res(ctx.status(200), ctx.json(billingSuccessResponse)),
), ),
rest.get('http://localhost/api/v1/dashboards', (_, res, ctx) =>
res(ctx.status(200), ctx.json(dashboardSuccessResponse)),
),
rest.get('http://localhost/api/v1/dashboards/4', (_, res, ctx) =>
res(ctx.status(200), ctx.json(getDashboardById)),
),
rest.get('http://localhost/api/v1/invite', (_, res, ctx) => rest.get('http://localhost/api/v1/invite', (_, res, ctx) =>
res(ctx.status(200), ctx.json(inviteUser)), res(ctx.status(200), ctx.json(inviteUser)),
), ),
rest.post('http://localhost/api/v1/invite', (_, res, ctx) => rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
res(ctx.status(200), ctx.json(inviteUser)), res(ctx.status(200), ctx.json(inviteUser)),
), ),
rest.get(
'http://localhost/api/v3/autocomplete/aggregate_attributes',
(req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: { attributeKeys: null },
}),
),
),
rest.get('http://localhost/api/v1/explorer/views', (req, res, ctx) =>
res(ctx.status(200), ctx.json(explorerView)),
),
rest.post('http://localhost/api/v1/event', (req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
statusCode: 200,
error: null,
payload: 'Event Processed Successfully',
}),
),
),
]; ];

View File

@ -0,0 +1,207 @@
/* eslint-disable sonarjs/no-duplicate-string */
import ROUTES from 'constants/routes';
import DashboardsList from 'container/ListOfDashboard';
import { dashboardEmptyState } from 'mocks-server/__mockdata__/dashboards';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { fireEvent, render, waitFor } from 'tests/test-utils';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
useRouteMatch: jest.fn().mockReturnValue({
params: {
dashboardId: 4,
},
}),
}));
const mockWindowOpen = jest.fn();
window.open = mockWindowOpen;
describe('dashboard list page', () => {
// should render on updatedAt and descend when the column key and order is messed up
it('should render the list even when the columnKey or the order is mismatched', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=asgard&order=stones&page=1`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText, getByTestId } = render(
<MemoryRouter
initialEntries={['/dashbords?columnKey=asgard&order=stones&page=1']}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
const firstElement = getByTestId('dashboard-title-0');
expect(firstElement.textContent).toBe('captain america');
const secondElement = getByTestId('dashboard-title-1');
expect(secondElement.textContent).toBe('thor');
});
// should render correctly when the column key is createdAt and order is descend
it('should render the list even when the columnKey and the order are given', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=createdAt&order=descend&page=1`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText, getByTestId } = render(
<MemoryRouter
initialEntries={['/dashbords?columnKey=createdAt&order=descend&page=1']}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
const firstElement = getByTestId('dashboard-title-0');
expect(firstElement.textContent).toBe('thor');
const secondElement = getByTestId('dashboard-title-1');
expect(secondElement.textContent).toBe('captain america');
});
// change the sort by order and dashboards list ot be updated accordingly
it('dashboards list should be correctly updated on choosing the different sortBy from dropdown values', async () => {
const { getByText, getByTestId } = render(
<MemoryRouter
initialEntries={[
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
const firstElement = getByTestId('dashboard-title-0');
expect(firstElement.textContent).toBe('thor');
const secondElement = getByTestId('dashboard-title-1');
expect(secondElement.textContent).toBe('captain america');
// click on the sort button
const sortByButton = getByTestId('sort-by');
expect(sortByButton).toBeInTheDocument();
fireEvent.click(sortByButton!);
// change the sort order
const sortByUpdatedBy = getByTestId('sort-by-last-updated');
await waitFor(() => expect(sortByUpdatedBy).toBeInTheDocument());
fireEvent.click(sortByUpdatedBy!);
// expect the new order
const updatedFirstElement = getByTestId('dashboard-title-0');
expect(updatedFirstElement.textContent).toBe('captain america');
const updatedSecondElement = getByTestId('dashboard-title-1');
expect(updatedSecondElement.textContent).toBe('thor');
});
// should filter correctly on search string
it('should filter dashboards based on search string', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=createdAt&order=descend&page=1&search=tho`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText, getByTestId, queryByText } = render(
<MemoryRouter
initialEntries={[
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() => expect(getByText('All Dashboards')).toBeInTheDocument());
const firstElement = getByTestId('dashboard-title-0');
expect(firstElement.textContent).toBe('thor');
expect(queryByText('captain america')).not.toBeInTheDocument();
// the pagination item should not be present in the list when number of items are less than one page size
expect(
document.querySelector('.ant-table-pagination'),
).not.toBeInTheDocument();
});
it('dashboard empty search state', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=createdAt&order=descend&page=1&search=someRandomString`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByText } = render(
<MemoryRouter
initialEntries={[
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() =>
expect(
getByText(
'No dashboards found for someRandomString. Create a new dashboard?',
),
).toBeInTheDocument(),
);
});
it('dashboard empty state', async () => {
const mockLocation = {
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.ALL_DASHBOARD}/`,
search: `columnKey=createdAt&order=descend&page=1`,
};
(useLocation as jest.Mock).mockReturnValue(mockLocation);
server.use(
rest.get('http://localhost/api/v1/dashboards', (_, res, ctx) =>
res(ctx.status(200), ctx.json(dashboardEmptyState)),
),
);
const { getByText, getByTestId } = render(
<MemoryRouter
initialEntries={[
'/dashbords?columnKey=createdAt&order=descend&page=1&search=tho',
]}
>
<DashboardProvider>
<DashboardsList />
</DashboardProvider>
</MemoryRouter>,
);
await waitFor(() =>
expect(getByText('No dashboards yet.')).toBeInTheDocument(),
);
const learnMoreButton = getByTestId('learn-more');
expect(learnMoreButton).toBeInTheDocument();
fireEvent.click(learnMoreButton);
// test the correct link to be added for the dashboards empty state
await waitFor(() =>
expect(mockWindowOpen).toHaveBeenCalledWith(
'https://signoz.io/docs/userguide/manage-dashboards?utm_source=product&utm_medium=dashboard-list-empty-state',
'_blank',
),
);
});
});

View File

@ -4,7 +4,6 @@ import { Button, Typography } from 'antd';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import cx from 'classnames'; import cx from 'classnames';
import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer'; import { MarkdownRenderer } from 'components/MarkdownRenderer/MarkdownRenderer';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils'; import { INTEGRATION_TELEMETRY_EVENTS } from 'pages/Integrations/utils';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -18,8 +17,6 @@ function Configure(props: ConfigurationProps): JSX.Element {
const { configuration, integrationId } = props; const { configuration, integrationId } = props;
const [selectedConfigStep, setSelectedConfigStep] = useState(0); const [selectedConfigStep, setSelectedConfigStep] = useState(0);
const { trackEvent } = useAnalytics();
const handleMenuClick = (index: number, config: any): void => { const handleMenuClick = (index: number, config: any): void => {
setSelectedConfigStep(index); setSelectedConfigStep(index);
logEvent('Integrations Detail Page: Configure tab', { logEvent('Integrations Detail Page: Configure tab', {
@ -29,7 +26,7 @@ function Configure(props: ConfigurationProps): JSX.Element {
}; };
useEffect(() => { useEffect(() => {
trackEvent( logEvent(
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION, INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONFIGURE_INSTRUCTION,
{ {
integration: integrationId, integration: integrationId,

View File

@ -2,12 +2,12 @@
import './IntegrationDetailPage.styles.scss'; import './IntegrationDetailPage.styles.scss';
import { Button, Modal, Tooltip, Typography } from 'antd'; import { Button, Modal, Tooltip, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import installIntegration from 'api/Integrations/installIntegration'; import installIntegration from 'api/Integrations/installIntegration';
import ConfigureIcon from 'assets/Integrations/ConfigureIcon'; import ConfigureIcon from 'assets/Integrations/ConfigureIcon';
import cx from 'classnames'; import cx from 'classnames';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { ArrowLeftRight, Check } from 'lucide-react'; import { ArrowLeftRight, Check } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
@ -43,8 +43,6 @@ function IntegrationDetailHeader(
} = props; } = props;
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { trackEvent } = useAnalytics();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const showModal = (): void => { const showModal = (): void => {
@ -137,11 +135,11 @@ function IntegrationDetailHeader(
disabled={isInstallLoading} disabled={isInstallLoading}
onClick={(): void => { onClick={(): void => {
if (connectionState === ConnectionStates.NotInstalled) { if (connectionState === ConnectionStates.NotInstalled) {
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONNECT, { logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_CONNECT, {
integration: id, integration: id,
}); });
} else { } else {
trackEvent( logEvent(
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_TEST_CONNECTION, INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_TEST_CONNECTION,
{ {
integration: id, integration: id,

View File

@ -1,9 +1,9 @@
import './IntegrationDetailPage.styles.scss'; import './IntegrationDetailPage.styles.scss';
import { Button, Modal, Typography } from 'antd'; import { Button, Modal, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import unInstallIntegration from 'api/Integrations/uninstallIntegration'; import unInstallIntegration from 'api/Integrations/uninstallIntegration';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import useAnalytics from 'hooks/analytics/useAnalytics';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { useState } from 'react'; import { useState } from 'react';
@ -30,8 +30,6 @@ function IntergrationsUninstallBar(
const { notifications } = useNotifications(); const { notifications } = useNotifications();
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { trackEvent } = useAnalytics();
const { const {
mutate: uninstallIntegration, mutate: uninstallIntegration,
isLoading: isUninstallLoading, isLoading: isUninstallLoading,
@ -52,7 +50,7 @@ function IntergrationsUninstallBar(
}; };
const handleOk = (): void => { const handleOk = (): void => {
trackEvent( logEvent(
INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_REMOVE_INTEGRATION, INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_DETAIL_REMOVE_INTEGRATION,
{ {
integration: integrationId, integration: integrationId,

View File

@ -1,6 +1,6 @@
import './Integrations.styles.scss'; import './Integrations.styles.scss';
import useAnalytics from 'hooks/analytics/useAnalytics'; import logEvent from 'api/common/logEvent';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
@ -16,8 +16,6 @@ function Integrations(): JSX.Element {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const { trackEvent } = useAnalytics();
const selectedIntegration = useMemo(() => urlQuery.get('integration'), [ const selectedIntegration = useMemo(() => urlQuery.get('integration'), [
urlQuery, urlQuery,
]); ]);
@ -25,7 +23,7 @@ function Integrations(): JSX.Element {
const setSelectedIntegration = useCallback( const setSelectedIntegration = useCallback(
(integration: string | null) => { (integration: string | null) => {
if (integration) { if (integration) {
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_ITEM_LIST_CLICKED, { logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_ITEM_LIST_CLICKED, {
integration, integration,
}); });
urlQuery.set('integration', integration); urlQuery.set('integration', integration);
@ -35,7 +33,7 @@ function Integrations(): JSX.Element {
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
history.push(generatedUrl); history.push(generatedUrl);
}, },
[history, location.pathname, trackEvent, urlQuery], [history, location.pathname, urlQuery],
); );
const [activeDetailTab, setActiveDetailTab] = useState<string | null>( const [activeDetailTab, setActiveDetailTab] = useState<string | null>(
@ -43,7 +41,7 @@ function Integrations(): JSX.Element {
); );
useEffect(() => { useEffect(() => {
trackEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_LIST_VISITED); logEvent(INTEGRATION_TELEMETRY_EVENTS.INTEGRATIONS_LIST_VISITED, {});
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);

View File

@ -149,11 +149,11 @@ function SaveView(): JSX.Element {
if (!logEventCalledRef.current && !isLoading) { if (!logEventCalledRef.current && !isLoading) {
if (sourcepage === DataSource.TRACES) { if (sourcepage === DataSource.TRACES) {
logEvent('Traces Views: Views visited', { logEvent('Traces Views: Views visited', {
number: viewsData?.data.data.length, number: viewsData?.data?.data?.length,
}); });
} else if (sourcepage === DataSource.LOGS) { } else if (sourcepage === DataSource.LOGS) {
logEvent('Logs Views: Views visited', { logEvent('Logs Views: Views visited', {
number: viewsData?.data.data.length, number: viewsData?.data?.data?.length,
}); });
} }
logEventCalledRef.current = true; logEventCalledRef.current = true;

View File

@ -26,7 +26,10 @@ export const getRoutes = (
settings.push(...organizationSettings(t)); settings.push(...organizationSettings(t));
} }
if (isGatewayEnabled && userRole === USER_ROLES.ADMIN) { if (
isGatewayEnabled &&
(userRole === USER_ROLES.ADMIN || userRole === USER_ROLES.EDITOR)
) {
settings.push(...multiIngestionSettings(t)); settings.push(...multiIngestionSettings(t));
} }

View File

@ -1,4 +1,5 @@
import { Button, Form, Input, Space, Switch, Typography } from 'antd'; import { Button, Form, Input, Space, Switch, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import editOrg from 'api/user/editOrg'; import editOrg from 'api/user/editOrg';
import getInviteDetails from 'api/user/getInviteDetails'; import getInviteDetails from 'api/user/getInviteDetails';
import loginApi from 'api/user/login'; import loginApi from 'api/user/login';
@ -7,7 +8,6 @@ import afterLogin from 'AppRoutes/utils';
import WelcomeLeftContainer from 'components/WelcomeLeftContainer'; import WelcomeLeftContainer from 'components/WelcomeLeftContainer';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import useAnalytics from 'hooks/analytics/useAnalytics';
import useFeatureFlag from 'hooks/useFeatureFlag'; import useFeatureFlag from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
@ -57,7 +57,6 @@ function SignUp({ version }: SignUpProps): JSX.Element {
false, false,
); );
const { search } = useLocation(); const { search } = useLocation();
const { trackEvent } = useAnalytics();
const params = new URLSearchParams(search); const params = new URLSearchParams(search);
const token = params.get('token'); const token = params.get('token');
const [isDetailsDisable, setIsDetailsDisable] = useState<boolean>(false); const [isDetailsDisable, setIsDetailsDisable] = useState<boolean>(false);
@ -88,7 +87,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
form.setFieldValue('organizationName', responseDetails.organization); form.setFieldValue('organizationName', responseDetails.organization);
setIsDetailsDisable(true); setIsDetailsDisable(true);
trackEvent('Account Creation Page Visited', { logEvent('Account Creation Page Visited', {
email: responseDetails.email, email: responseDetails.email,
name: responseDetails.name, name: responseDetails.name,
company_name: responseDetails.organization, company_name: responseDetails.organization,
@ -241,7 +240,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
setLoading(true); setLoading(true);
if (!isPasswordValid(values.password)) { if (!isPasswordValid(values.password)) {
trackEvent('Account Creation Page - Invalid Password', { logEvent('Account Creation Page - Invalid Password', {
email: values.email, email: values.email,
name: values.firstName, name: values.firstName,
}); });
@ -253,7 +252,7 @@ function SignUp({ version }: SignUpProps): JSX.Element {
if (isPreferenceVisible) { if (isPreferenceVisible) {
await commonHandler(values, onAdminAfterLogin); await commonHandler(values, onAdminAfterLogin);
} else { } else {
trackEvent('Account Created Successfully', { logEvent('Account Created Successfully', {
email: values.email, email: values.email,
name: values.firstName, name: values.firstName,
}); });

View File

@ -1,7 +1,7 @@
import './Support.styles.scss'; import './Support.styles.scss';
import { Button, Card, Typography } from 'antd'; import { Button, Card, Typography } from 'antd';
import useAnalytics from 'hooks/analytics/useAnalytics'; import logEvent from 'api/common/logEvent';
import { import {
Book, Book,
Cable, Cable,
@ -85,7 +85,6 @@ const supportChannels = [
]; ];
export default function Support(): JSX.Element { export default function Support(): JSX.Element {
const { trackEvent } = useAnalytics();
const history = useHistory(); const history = useHistory();
const handleChannelWithRedirects = (url: string): void => { const handleChannelWithRedirects = (url: string): void => {
@ -97,7 +96,7 @@ export default function Support(): JSX.Element {
const histroyState = history?.location?.state as any; const histroyState = history?.location?.state as any;
if (histroyState && histroyState?.from) { if (histroyState && histroyState?.from) {
trackEvent(`Support : From URL : ${histroyState.from}`); logEvent(`Support : From URL : ${histroyState.from}`, {});
} }
} }
@ -129,7 +128,7 @@ export default function Support(): JSX.Element {
}; };
const handleChannelClick = (channel: Channel): void => { const handleChannelClick = (channel: Channel): void => {
trackEvent(`Support : ${channel.name}`); logEvent(`Support : ${channel.name}`, {});
switch (channel.key) { switch (channel.key) {
case channelsMap.documentation: case channelsMap.documentation:

View File

@ -109,6 +109,7 @@ export function DurationSection(props: DurationProps): JSX.Element {
className="min-max-input" className="min-max-input"
onChange={onChangeMinHandler} onChange={onChangeMinHandler}
value={preMin} value={preMin}
data-testid="min-input"
addonAfter="ms" addonAfter="ms"
/> />
<Input <Input
@ -118,6 +119,7 @@ export function DurationSection(props: DurationProps): JSX.Element {
className="min-max-input" className="min-max-input"
onChange={onChangeMaxHandler} onChange={onChangeMaxHandler}
value={preMax} value={preMax}
data-testid="max-input"
addonAfter="ms" addonAfter="ms"
/> />
</div> </div>

View File

@ -224,13 +224,18 @@ export function Filter(props: FilterProps): JSX.Element {
<Button <Button
onClick={(): void => handleRun({ resetAll: true })} onClick={(): void => handleRun({ resetAll: true })}
className="sync-icon" className="sync-icon"
data-testid="reset-filters"
> >
<SyncOutlined /> <SyncOutlined />
</Button> </Button>
</Tooltip> </Tooltip>
</Flex> </Flex>
<Tooltip title="Collapse" placement="right"> <Tooltip title="Collapse" placement="right">
<Button onClick={(): void => setOpen(false)} className="arrow-icon"> <Button
onClick={(): void => setOpen(false)}
className="arrow-icon"
data-testid="toggle-filter-panel"
>
<VerticalAlignTopOutlined rotate={270} /> <VerticalAlignTopOutlined rotate={270} />
</Button> </Button>
</Tooltip> </Tooltip>

View File

@ -64,7 +64,7 @@ export function Section(props: SectionProps): JSX.Element {
return ( return (
<div> <div>
<Divider plain className="divider" /> <Divider plain className="divider" />
<div className="section-body-header"> <div className="section-body-header" data-testid={`collapse-${panelName}`}>
<Collapse <Collapse
bordered={false} bordered={false}
className="collapseContainer" className="collapseContainer"
@ -96,7 +96,11 @@ export function Section(props: SectionProps): JSX.Element {
}, },
]} ]}
/> />
<Button type="link" onClick={onClearHandler}> <Button
type="link"
onClick={onClearHandler}
data-testid={`collapse-${panelName}-clearBtn`}
>
Clear All Clear All
</Button> </Button>
</div> </div>

View File

@ -145,6 +145,7 @@ export function SectionBody(props: SectionBodyProps): JSX.Element {
key={`${type}-${item}`} key={`${type}-${item}`}
onChange={(e): void => onCheckHandler(e, item)} onChange={(e): void => onCheckHandler(e, item)}
checked={checkboxMatcher(item)} checked={checkboxMatcher(item)}
data-testid={`${type}-${item}`}
> >
<div className="checkbox-label"> <div className="checkbox-label">
<div className={labelClassname(item)} /> <div className={labelClassname(item)} />

View File

@ -1,16 +1,19 @@
/* eslint-disable sonarjs/no-duplicate-string */ /* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable no-restricted-syntax */ /* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import userEvent from '@testing-library/user-event';
import { import {
initialQueriesMap, initialQueriesMap,
initialQueryBuilderFormValues, initialQueryBuilderFormValues,
} from 'constants/queryBuilder'; } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam'; import * as compositeQueryHook from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { render } from 'tests/test-utils'; import { QueryBuilderContext } from 'providers/QueryBuilder';
import { fireEvent, render, screen, waitFor, within } from 'tests/test-utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { Query } from 'types/api/queryBuilder/queryBuilderData';
import TracesExplorer from '..';
import { Filter } from '../Filter/Filter'; import { Filter } from '../Filter/Filter';
import { AllTraceFilterKeyValue } from '../Filter/filterUtils'; import { AllTraceFilterKeyValue } from '../Filter/filterUtils';
@ -37,6 +40,48 @@ jest.mock('uplot', () => {
}; };
}); });
jest.mock(
'container/TopNav/DateTimeSelectionV2/index.tsx',
() =>
function MockDateTimeSelection(): JSX.Element {
return <div>MockDateTimeSelection</div>;
},
);
function checkIfSectionIsOpen(
getByTestId: (testId: string) => HTMLElement,
panelName: string,
): void {
const section = getByTestId(`collapse-${panelName}`);
expect(section.querySelector('.ant-collapse-item-active')).not.toBeNull();
}
function checkIfSectionIsNotOpen(
getByTestId: (testId: string) => HTMLElement,
panelName: string,
): void {
const section = getByTestId(`collapse-${panelName}`);
expect(section.querySelector('.ant-collapse-item-active')).toBeNull();
}
const defaultOpenSections = ['hasError', 'durationNano', 'serviceName'];
const defaultClosedSections = Object.keys(AllTraceFilterKeyValue).filter(
(section) =>
![...defaultOpenSections, 'durationNanoMin', 'durationNanoMax'].includes(
section,
),
);
async function checkForSectionContent(values: string[]): Promise<void> {
for (const val of values) {
const sectionContent = await screen.findByText(val);
await waitFor(() => expect(sectionContent).toBeInTheDocument());
}
}
const redirectWithQueryBuilderData = jest.fn();
const compositeQuery: Query = { const compositeQuery: Query = {
...initialQueriesMap.traces, ...initialQueriesMap.traces,
builder: { builder: {
@ -81,6 +126,157 @@ const compositeQuery: Query = {
}; };
describe('TracesExplorer - ', () => { describe('TracesExplorer - ', () => {
// Initial filter panel rendering
// Test the initial state like which filters section are opened, default state of duration slider, etc.
it('should render the Trace filter', async () => {
const { getByText, getByTestId } = render(<Filter setOpen={jest.fn()} />);
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
expect(getByText(filter)).toBeInTheDocument();
});
// Check default state of duration slider
const minDuration = getByTestId('min-input') as HTMLInputElement;
const maxDuration = getByTestId('max-input') as HTMLInputElement;
expect(minDuration).toHaveValue(null);
expect(minDuration).toHaveProperty('placeholder', '0');
expect(maxDuration).toHaveValue(null);
expect(maxDuration).toHaveProperty('placeholder', '100000000');
// Check which all filter section are opened by default
defaultOpenSections.forEach((section) =>
checkIfSectionIsOpen(getByTestId, section),
);
// Check which all filter section are closed by default
defaultClosedSections.forEach((section) =>
checkIfSectionIsNotOpen(getByTestId, section),
);
// check for the status section content
await checkForSectionContent(['Ok', 'Error']);
// check for the service name section content from API response
await checkForSectionContent([
'customer',
'demo-app',
'driver',
'frontend',
'mysql',
'redis',
'route',
'go-grpc-otel-server',
'test',
]);
});
// test the filter panel actions like opening and closing the sections, etc.
it('filter panel actions', async () => {
const { getByTestId } = render(<Filter setOpen={jest.fn()} />);
// Check if the section is closed
checkIfSectionIsNotOpen(getByTestId, 'name');
// Open the section
const name = getByTestId('collapse-name');
expect(name).toBeInTheDocument();
userEvent.click(within(name).getByText(AllTraceFilterKeyValue.name));
await waitFor(() => checkIfSectionIsOpen(getByTestId, 'name'));
await checkForSectionContent([
'HTTP GET',
'HTTP GET /customer',
'HTTP GET /dispatch',
'HTTP GET /route',
]);
// Close the section
userEvent.click(within(name).getByText(AllTraceFilterKeyValue.name));
await waitFor(() => checkIfSectionIsNotOpen(getByTestId, 'name'));
});
it('checking filters should update the query', async () => {
const { getByText } = render(
<QueryBuilderContext.Provider
value={
{
currentQuery: {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [initialQueryBuilderFormValues],
},
},
redirectWithQueryBuilderData,
} as any
}
>
<Filter setOpen={jest.fn()} />
</QueryBuilderContext.Provider>,
);
const okCheckbox = getByText('Ok');
fireEvent.click(okCheckbox);
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: {
id: expect.any(String),
key: 'hasError',
type: 'tag',
dataType: 'bool',
isColumn: true,
isJSON: false,
},
op: 'in',
value: ['false'],
}),
]),
);
// Check if the query is updated when the error checkbox is clicked
const errorCheckbox = getByText('Error');
fireEvent.click(errorCheckbox);
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: {
id: expect.any(String),
key: 'hasError',
type: 'tag',
dataType: 'bool',
isColumn: true,
isJSON: false,
},
op: 'in',
value: ['false', 'true'],
}),
]),
);
});
it('should render the trace filter with the given query', async () => {
jest
.spyOn(compositeQueryHook, 'useGetCompositeQueryParam')
.mockReturnValue(compositeQuery);
const { findByText, getByTestId } = render(<Filter setOpen={jest.fn()} />);
// check if the default query is applied - composite query has filters - serviceName : demo-app and name : HTTP GET /customer
expect(await findByText('demo-app')).toBeInTheDocument();
expect(getByTestId('serviceName-demo-app')).toBeChecked();
expect(await findByText('HTTP GET /customer')).toBeInTheDocument();
expect(getByTestId('name-HTTP GET /customer')).toBeChecked();
});
it('test edge cases of undefined filters', async () => { it('test edge cases of undefined filters', async () => {
jest.spyOn(compositeQueryHook, 'useGetCompositeQueryParam').mockReturnValue({ jest.spyOn(compositeQueryHook, 'useGetCompositeQueryParam').mockReturnValue({
...compositeQuery, ...compositeQuery,
@ -98,7 +294,6 @@ describe('TracesExplorer - ', () => {
const { getByText } = render(<Filter setOpen={jest.fn()} />); const { getByText } = render(<Filter setOpen={jest.fn()} />);
// we should have all the filters
Object.values(AllTraceFilterKeyValue).forEach((filter) => { Object.values(AllTraceFilterKeyValue).forEach((filter) => {
expect(getByText(filter)).toBeInTheDocument(); expect(getByText(filter)).toBeInTheDocument();
}); });
@ -124,9 +319,141 @@ describe('TracesExplorer - ', () => {
const { getByText } = render(<Filter setOpen={jest.fn()} />); const { getByText } = render(<Filter setOpen={jest.fn()} />);
// we should have all the filters
Object.values(AllTraceFilterKeyValue).forEach((filter) => { Object.values(AllTraceFilterKeyValue).forEach((filter) => {
expect(getByText(filter)).toBeInTheDocument(); expect(getByText(filter)).toBeInTheDocument();
}); });
}); });
it('should clear filter on clear & reset button click', async () => {
const { getByText, getByTestId } = render(
<QueryBuilderContext.Provider
value={
{
currentQuery: {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [initialQueryBuilderFormValues],
},
},
redirectWithQueryBuilderData,
} as any
}
>
<Filter setOpen={jest.fn()} />
</QueryBuilderContext.Provider>,
);
// check for the status section content
await checkForSectionContent(['Ok', 'Error']);
// check for the service name section content from API response
await checkForSectionContent([
'customer',
'demo-app',
'driver',
'frontend',
'mysql',
'redis',
'route',
'go-grpc-otel-server',
'test',
]);
const okCheckbox = getByText('Ok');
fireEvent.click(okCheckbox);
const frontendCheckbox = getByText('frontend');
fireEvent.click(frontendCheckbox);
// check if checked and present in query
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).toEqual(
expect.arrayContaining([
expect.objectContaining({
key: {
id: expect.any(String),
key: 'hasError',
type: 'tag',
dataType: 'bool',
isColumn: true,
isJSON: false,
},
op: 'in',
value: ['false'],
}),
expect.objectContaining({
key: {
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: expect.any(String),
},
op: 'in',
value: ['frontend'],
}),
]),
);
const clearButton = getByTestId('collapse-serviceName-clearBtn');
expect(clearButton).toBeInTheDocument();
fireEvent.click(clearButton);
// check if cleared and not present in query
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
key: {
key: 'serviceName',
dataType: 'string',
type: 'tag',
isColumn: true,
isJSON: false,
id: expect.any(String),
},
op: 'in',
value: ['frontend'],
}),
]),
);
// check if reset button is present
const resetButton = getByTestId('reset-filters');
expect(resetButton).toBeInTheDocument();
fireEvent.click(resetButton);
// check if reset id done
expect(
redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0].builder.queryData[0].filters.items,
).toEqual([]);
});
it('filter panel should collapse & uncollapsed', async () => {
const { getByText, getByTestId } = render(<TracesExplorer />);
Object.values(AllTraceFilterKeyValue).forEach((filter) => {
expect(getByText(filter)).toBeInTheDocument();
});
// Filter panel should collapse
const collapseButton = getByTestId('toggle-filter-panel');
expect(collapseButton).toBeInTheDocument();
fireEvent.click(collapseButton);
// uncollapse btn should be present
expect(
await screen.findByTestId('filter-uncollapse-btn'),
).toBeInTheDocument();
});
}); });

View File

@ -251,6 +251,7 @@ function TracesExplorer(): JSX.Element {
<Button <Button
onClick={(): void => setOpen(!isOpen)} onClick={(): void => setOpen(!isOpen)}
className="filter-outlined-btn" className="filter-outlined-btn"
data-testid="filter-uncollapse-btn"
> >
<FilterOutlined /> <FilterOutlined />
</Button> </Button>

View File

@ -8,10 +8,10 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Button, Card, Skeleton, Typography } from 'antd'; import { Button, Card, Skeleton, Typography } from 'antd';
import updateCreditCardApi from 'api/billing/checkout'; import updateCreditCardApi from 'api/billing/checkout';
import logEvent from 'api/common/logEvent';
import { SOMETHING_WENT_WRONG } from 'constants/api'; import { SOMETHING_WENT_WRONG } from 'constants/api';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader'; import FullScreenHeader from 'container/FullScreenHeader/FullScreenHeader';
import useAnalytics from 'hooks/analytics/useAnalytics';
import useLicense from 'hooks/useLicense'; import useLicense from 'hooks/useLicense';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history'; import history from 'lib/history';
@ -27,7 +27,6 @@ export default function WorkspaceBlocked(): JSX.Element {
const { role } = useSelector<AppState, AppReducer>((state) => state.app); const { role } = useSelector<AppState, AppReducer>((state) => state.app);
const isAdmin = role === 'ADMIN'; const isAdmin = role === 'ADMIN';
const [activeLicense, setActiveLicense] = useState<License | null>(null); const [activeLicense, setActiveLicense] = useState<License | null>(null);
const { trackEvent } = useAnalytics();
const { notifications } = useNotifications(); const { notifications } = useNotifications();
@ -74,7 +73,7 @@ export default function WorkspaceBlocked(): JSX.Element {
); );
const handleUpdateCreditCard = useCallback(async () => { const handleUpdateCreditCard = useCallback(async () => {
trackEvent('Workspace Blocked: User Clicked Update Credit Card'); logEvent('Workspace Blocked: User Clicked Update Credit Card', {});
updateCreditCard({ updateCreditCard({
licenseKey: activeLicense?.key || '', licenseKey: activeLicense?.key || '',
@ -85,7 +84,7 @@ export default function WorkspaceBlocked(): JSX.Element {
}, [activeLicense?.key, updateCreditCard]); }, [activeLicense?.key, updateCreditCard]);
const handleExtendTrial = (): void => { const handleExtendTrial = (): void => {
trackEvent('Workspace Blocked: User Clicked Extend Trial'); logEvent('Workspace Blocked: User Clicked Extend Trial', {});
notifications.info({ notifications.info({
message: 'Extend Trial', message: 'Extend Trial',

View File

@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
import { Modal } from 'antd'; import { Modal } from 'antd';
import getDashboard from 'api/dashboard/get'; import getDashboard from 'api/dashboard/get';
import lockDashboardApi from 'api/dashboard/lockDashboard'; import lockDashboardApi from 'api/dashboard/lockDashboard';
@ -11,6 +12,7 @@ import useAxiosError from 'hooks/useAxiosError';
import useTabVisibility from 'hooks/useTabFocus'; import useTabVisibility from 'hooks/useTabFocus';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout'; import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
import history from 'lib/history';
import { defaultTo } from 'lodash-es'; import { defaultTo } from 'lodash-es';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
import isUndefined from 'lodash-es/isUndefined'; import isUndefined from 'lodash-es/isUndefined';
@ -38,7 +40,7 @@ import AppReducer from 'types/reducer/app';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as generateUUID } from 'uuid'; import { v4 as generateUUID } from 'uuid';
import { IDashboardContext } from './types'; import { DashboardSortOrder, IDashboardContext } from './types';
import { sortLayout } from './util'; import { sortLayout } from './util';
const DashboardContext = createContext<IDashboardContext>({ const DashboardContext = createContext<IDashboardContext>({
@ -52,7 +54,12 @@ const DashboardContext = createContext<IDashboardContext>({
layouts: [], layouts: [],
panelMap: {}, panelMap: {},
setPanelMap: () => {}, setPanelMap: () => {},
listSortOrder: { columnKey: 'createdAt', order: 'descend', pagination: '1' }, listSortOrder: {
columnKey: 'createdAt',
order: 'descend',
pagination: '1',
search: '',
},
setListSortOrder: () => {}, setListSortOrder: () => {},
setLayouts: () => {}, setLayouts: () => {},
setSelectedDashboard: () => {}, setSelectedDashboard: () => {},
@ -68,6 +75,7 @@ interface Props {
dashboardId: string; dashboardId: string;
} }
// eslint-disable-next-line sonarjs/cognitive-complexity
export function DashboardProvider({ export function DashboardProvider({
children, children,
}: PropsWithChildren): JSX.Element { }: PropsWithChildren): JSX.Element {
@ -82,17 +90,50 @@ export function DashboardProvider({
exact: true, exact: true,
}); });
const params = useUrlQuery(); const isDashboardListPage = useRouteMatch<Props>({
const orderColumnParam = params.get('columnKey'); path: ROUTES.ALL_DASHBOARD,
const orderQueryParam = params.get('order'); exact: true,
const paginationParam = params.get('page');
const [listSortOrder, setListSortOrder] = useState({
columnKey: orderColumnParam || 'updatedAt',
order: orderQueryParam || 'descend',
pagination: paginationParam || '1',
}); });
// added extra checks here in case wrong values appear use the default values rather than empty dashboards
const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
const supportedOrderKeys = ['ascend', 'descend'];
const params = useUrlQuery();
// since the dashboard provider is wrapped at the very top of the application hence it initialises these values from other pages as well.
// pick the below params from URL only if the user is on the dashboards list page.
const orderColumnParam = isDashboardListPage && params.get('columnKey');
const orderQueryParam = isDashboardListPage && params.get('order');
const paginationParam = isDashboardListPage && params.get('page');
const searchParam = isDashboardListPage && params.get('search');
const [listSortOrder, setListOrder] = useState({
columnKey: orderColumnParam
? supportedOrderColumnKeys.includes(orderColumnParam)
? orderColumnParam
: 'updatedAt'
: 'updatedAt',
order: orderQueryParam
? supportedOrderKeys.includes(orderQueryParam)
? orderQueryParam
: 'descend'
: 'descend',
pagination: paginationParam || '1',
search: searchParam || '',
});
function setListSortOrder(sortOrder: DashboardSortOrder): void {
if (!isEqual(sortOrder, listSortOrder)) {
setListOrder(sortOrder);
}
params.set('columnKey', sortOrder.columnKey as string);
params.set('order', sortOrder.order as string);
params.set('page', sortOrder.pagination || '1');
params.set('search', sortOrder.search || '');
history.replace({ search: params.toString() });
}
const dispatch = useDispatch<Dispatch<AppActions>>(); const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>( const globalTime = useSelector<AppState, GlobalReducer>(

View File

@ -1,9 +1,15 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { Dispatch, SetStateAction } from 'react';
import { Layout } from 'react-grid-layout'; import { Layout } from 'react-grid-layout';
import { UseQueryResult } from 'react-query'; import { UseQueryResult } from 'react-query';
import { Dashboard } from 'types/api/dashboard/getAll'; import { Dashboard } from 'types/api/dashboard/getAll';
export interface DashboardSortOrder {
columnKey: string;
order: string;
pagination: string;
search: string;
}
export interface IDashboardContext { export interface IDashboardContext {
isDashboardSliderOpen: boolean; isDashboardSliderOpen: boolean;
isDashboardLocked: boolean; isDashboardLocked: boolean;
@ -15,18 +21,8 @@ export interface IDashboardContext {
layouts: Layout[]; layouts: Layout[];
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>; panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>; setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
listSortOrder: { listSortOrder: DashboardSortOrder;
columnKey: string; setListSortOrder: (sortOrder: DashboardSortOrder) => void;
order: string;
pagination: string;
};
setListSortOrder: Dispatch<
SetStateAction<{
columnKey: string;
order: string;
pagination: string;
}>
>;
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>; setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
setSelectedDashboard: React.Dispatch< setSelectedDashboard: React.Dispatch<
React.SetStateAction<Dashboard | undefined> React.SetStateAction<Dashboard | undefined>

View File

@ -27,7 +27,7 @@ import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName'; import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType'; import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
import { replaceIncorrectObjectFields } from 'lib/replaceIncorrectObjectFields'; import { replaceIncorrectObjectFields } from 'lib/replaceIncorrectObjectFields';
import { get, merge, set } from 'lodash-es'; import { cloneDeep, get, merge, set } from 'lodash-es';
import { import {
createContext, createContext,
PropsWithChildren, PropsWithChildren,
@ -532,7 +532,7 @@ export function QueryBuilderProvider({
if (!panelType) { if (!panelType) {
return newQueryItem; return newQueryItem;
} }
const queryItem = item as IBuilderQuery; const queryItem = cloneDeep(item) as IBuilderQuery;
const propsRequired = const propsRequired =
panelTypeDataSourceFormValuesMap[panelType as keyof PartialPanelTypes]?.[ panelTypeDataSourceFormValuesMap[panelType as keyof PartialPanelTypes]?.[
queryItem.dataSource queryItem.dataSource
@ -829,7 +829,7 @@ export function QueryBuilderProvider({
unit, unit,
})); }));
}, },
[setCurrentQuery], [setCurrentQuery, setSupersetQuery],
); );
const query: Query = useMemo( const query: Query = useMemo(

View File

@ -1,3 +1,4 @@
import ROUTES from 'constants/routes';
import { parseQuery } from 'lib/logql'; import { parseQuery } from 'lib/logql';
import { OrderPreferenceItems } from 'pages/Logs/config'; import { OrderPreferenceItems } from 'pages/Logs/config';
import { import {
@ -29,6 +30,30 @@ import {
} from 'types/actions/logs'; } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs'; import { ILogsReducer } from 'types/reducer/logs';
const supportedLogsOrder = [
OrderPreferenceItems.ASC,
OrderPreferenceItems.DESC,
];
function getLogsOrder(): OrderPreferenceItems {
// set the value of order from the URL only when order query param is present and the user is landing on the old logs explorer page
if (window.location.pathname === ROUTES.OLD_LOGS_EXPLORER) {
const orderParam = new URLSearchParams(window.location.search).get('order');
if (orderParam) {
// check if the order passed is supported else pass the default order
if (supportedLogsOrder.includes(orderParam as OrderPreferenceItems)) {
return orderParam as OrderPreferenceItems;
}
return OrderPreferenceItems.DESC;
}
return OrderPreferenceItems.DESC;
}
return OrderPreferenceItems.DESC;
}
const initialState: ILogsReducer = { const initialState: ILogsReducer = {
fields: { fields: {
interesting: [], interesting: [],
@ -51,10 +76,7 @@ const initialState: ILogsReducer = {
liveTailStartRange: 15, liveTailStartRange: 15,
selectedLogId: null, selectedLogId: null,
detailedLog: null, detailedLog: null,
order: order: getLogsOrder(),
(new URLSearchParams(window.location.search).get(
'order',
) as ILogsReducer['order']) ?? OrderPreferenceItems.DESC,
}; };
export const LogsReducer = ( export const LogsReducer = (

View File

@ -42,6 +42,7 @@ const mockStored = (role?: string): any =>
accessJwt: '', accessJwt: '',
refreshJwt: '', refreshJwt: '',
}, },
isLoggedIn: true,
org: [ org: [
{ {
createdAt: 0, createdAt: 0,

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.20.0 github.com/ClickHouse/clickhouse-go/v2 v2.20.0
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.102.2 github.com/SigNoz/signoz-otel-collector v0.102.3
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974
github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974 github.com/SigNoz/zap_otlp/zap_otlp_sync v0.0.0-20230822164844-1b861a431974
github.com/antonmedv/expr v1.15.3 github.com/antonmedv/expr v1.15.3

4
go.sum
View File

@ -64,8 +64,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc= github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg= github.com/SigNoz/prometheus v1.11.1 h1:roM8ugYf4UxaeKKujEeBvoX7ybq3IrS+TB26KiRtIJg=
github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I= github.com/SigNoz/prometheus v1.11.1/go.mod h1:uv4mQwZQtx7y4GQ6EdHOi8Wsk07uHNn2XHd1zM85m6I=
github.com/SigNoz/signoz-otel-collector v0.102.2 h1:SmjsBZjMjTVVpuOlfJXlsDJQbdefQP/9Wz3CyzSuZuU= github.com/SigNoz/signoz-otel-collector v0.102.3 h1:q6iS5kqqwopwC2pS2UvYL3IiJMP75UdyK6d+rculXn4=
github.com/SigNoz/signoz-otel-collector v0.102.2/go.mod h1:ISAXYhZenojCWg6CdDJtPMpfS6Zwc08+uoxH25tc6Y0= github.com/SigNoz/signoz-otel-collector v0.102.3/go.mod h1:61WqwhnrtFjwj1FyfDYMXjxFx8gWgKok1Xy1C6LbjWo=
github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc= github.com/SigNoz/zap_otlp v0.1.0 h1:T7rRcFN87GavY8lDGZj0Z3Xv6OhJA6Pj3I9dNPmqvRc=
github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo= github.com/SigNoz/zap_otlp v0.1.0/go.mod h1:lcHvbDbRgvDnPxo9lDlaL1JK2PyOyouP/C3ynnYIvyo=
github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY= github.com/SigNoz/zap_otlp/zap_otlp_encoder v0.0.0-20230822164844-1b861a431974 h1:PKVgdf83Yw+lZJbFtNGBgqXiXNf3+kOXW2qZ7Ms7OaY=

View File

@ -706,21 +706,25 @@ func (r *ClickHouseReader) GetServicesList(ctx context.Context) (*[]string, erro
return &services, nil return &services, nil
} }
func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, skipConfig *model.SkipConfig, start, end time.Time) (*map[string][]string, *map[string][]string, *model.ApiError) { func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, skipConfig *model.SkipConfig, start, end time.Time, services []string) (*map[string][]string, *model.ApiError) {
start = start.In(time.UTC) start = start.In(time.UTC)
// The `top_level_operations` that have `time` >= start // The `top_level_operations` that have `time` >= start
operations := map[string][]string{} operations := map[string][]string{}
// All top level operations for a service // We can't use the `end` because the `top_level_operations` table has the most recent instances of the operations
allOperations := map[string][]string{} // We can only use the `start` time to filter the operations
query := fmt.Sprintf(`SELECT DISTINCT name, serviceName, time FROM %s.%s`, r.TraceDB, r.topLevelOperationsTable) query := fmt.Sprintf(`SELECT name, serviceName, max(time) as ts FROM %s.%s WHERE time >= @start`, r.TraceDB, r.topLevelOperationsTable)
if len(services) > 0 {
query += ` AND serviceName IN @services`
}
query += ` GROUP BY name, serviceName ORDER BY ts DESC LIMIT 5000`
rows, err := r.db.Query(ctx, query) rows, err := r.db.Query(ctx, query, clickhouse.Named("start", start), clickhouse.Named("services", services))
if err != nil { if err != nil {
zap.L().Error("Error in processing sql query", zap.Error(err)) zap.L().Error("Error in processing sql query", zap.Error(err))
return nil, nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")} return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query")}
} }
defer rows.Close() defer rows.Close()
@ -728,25 +732,17 @@ func (r *ClickHouseReader) GetTopLevelOperations(ctx context.Context, skipConfig
var name, serviceName string var name, serviceName string
var t time.Time var t time.Time
if err := rows.Scan(&name, &serviceName, &t); err != nil { if err := rows.Scan(&name, &serviceName, &t); err != nil {
return nil, nil, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error in reading data")} return nil, &model.ApiError{Typ: model.ErrorInternal, Err: fmt.Errorf("error in reading data")}
} }
if _, ok := operations[serviceName]; !ok { if _, ok := operations[serviceName]; !ok {
operations[serviceName] = []string{} operations[serviceName] = []string{"overflow_operation"}
}
if _, ok := allOperations[serviceName]; !ok {
allOperations[serviceName] = []string{}
} }
if skipConfig.ShouldSkip(serviceName, name) { if skipConfig.ShouldSkip(serviceName, name) {
continue continue
} }
allOperations[serviceName] = append(allOperations[serviceName], name) operations[serviceName] = append(operations[serviceName], name)
// We can't use the `end` because the `top_level_operations` table has the most recent instances of the operations
// We can only use the `start` time to filter the operations
if t.After(start) {
operations[serviceName] = append(operations[serviceName], name)
}
} }
return &operations, &allOperations, nil return &operations, nil
} }
func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.GetServicesParams, skipConfig *model.SkipConfig) (*[]model.ServiceItem, *model.ApiError) { func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.GetServicesParams, skipConfig *model.SkipConfig) (*[]model.ServiceItem, *model.ApiError) {
@ -755,7 +751,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G
return nil, &model.ApiError{Typ: model.ErrorExec, Err: ErrNoIndexTable} return nil, &model.ApiError{Typ: model.ErrorExec, Err: ErrNoIndexTable}
} }
topLevelOps, allTopLevelOps, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End) topLevelOps, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End, nil)
if apiErr != nil { if apiErr != nil {
return nil, apiErr return nil, apiErr
} }
@ -779,7 +775,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G
// the top level operations are high, we want to warn to let user know the issue // the top level operations are high, we want to warn to let user know the issue
// with the instrumentation // with the instrumentation
serviceItem.DataWarning = model.DataWarning{ serviceItem.DataWarning = model.DataWarning{
TopLevelOps: (*allTopLevelOps)[svc], TopLevelOps: (*topLevelOps)[svc],
} }
// default max_query_size = 262144 // default max_query_size = 262144
@ -868,7 +864,7 @@ func (r *ClickHouseReader) GetServices(ctx context.Context, queryParams *model.G
func (r *ClickHouseReader) GetServiceOverview(ctx context.Context, queryParams *model.GetServiceOverviewParams, skipConfig *model.SkipConfig) (*[]model.ServiceOverviewItem, *model.ApiError) { func (r *ClickHouseReader) GetServiceOverview(ctx context.Context, queryParams *model.GetServiceOverviewParams, skipConfig *model.SkipConfig) (*[]model.ServiceOverviewItem, *model.ApiError) {
topLevelOps, _, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End) topLevelOps, apiErr := r.GetTopLevelOperations(ctx, skipConfig, *queryParams.Start, *queryParams.End, nil)
if apiErr != nil { if apiErr != nil {
return nil, apiErr return nil, apiErr
} }
@ -5005,3 +5001,27 @@ func (r *ClickHouseReader) LiveTailLogsV3(ctx context.Context, query string, tim
} }
} }
} }
func (r *ClickHouseReader) GetMinAndMaxTimestampForTraceID(ctx context.Context, traceID []string) (int64, int64, error) {
var minTime, maxTime time.Time
query := fmt.Sprintf("SELECT min(timestamp), max(timestamp) FROM %s.%s WHERE traceID IN ('%s')",
r.TraceDB, r.SpansTable, strings.Join(traceID, "','"))
zap.L().Debug("GetMinAndMaxTimestampForTraceID", zap.String("query", query))
err := r.db.QueryRow(ctx, query).Scan(&minTime, &maxTime)
if err != nil {
zap.L().Error("Error while executing query", zap.Error(err))
return 0, 0, err
}
if minTime.IsZero() || maxTime.IsZero() {
zap.L().Debug("minTime or maxTime is zero")
return 0, 0, nil
}
zap.L().Debug("GetMinAndMaxTimestampForTraceID", zap.Any("minTime", minTime), zap.Any("maxTime", maxTime))
return minTime.UnixNano(), maxTime.UnixNano(), nil
}

View File

@ -29,12 +29,14 @@ import (
logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3" logsv3 "go.signoz.io/signoz/pkg/query-service/app/logs/v3"
"go.signoz.io/signoz/pkg/query-service/app/metrics" "go.signoz.io/signoz/pkg/query-service/app/metrics"
metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3" metricsv3 "go.signoz.io/signoz/pkg/query-service/app/metrics/v3"
"go.signoz.io/signoz/pkg/query-service/app/preferences"
"go.signoz.io/signoz/pkg/query-service/app/querier" "go.signoz.io/signoz/pkg/query-service/app/querier"
querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2" querierV2 "go.signoz.io/signoz/pkg/query-service/app/querier/v2"
"go.signoz.io/signoz/pkg/query-service/app/queryBuilder" "go.signoz.io/signoz/pkg/query-service/app/queryBuilder"
tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3" tracesV3 "go.signoz.io/signoz/pkg/query-service/app/traces/v3"
"go.signoz.io/signoz/pkg/query-service/auth" "go.signoz.io/signoz/pkg/query-service/auth"
"go.signoz.io/signoz/pkg/query-service/cache" "go.signoz.io/signoz/pkg/query-service/cache"
"go.signoz.io/signoz/pkg/query-service/common"
"go.signoz.io/signoz/pkg/query-service/constants" "go.signoz.io/signoz/pkg/query-service/constants"
v3 "go.signoz.io/signoz/pkg/query-service/model/v3" v3 "go.signoz.io/signoz/pkg/query-service/model/v3"
"go.signoz.io/signoz/pkg/query-service/postprocess" "go.signoz.io/signoz/pkg/query-service/postprocess"
@ -398,6 +400,22 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) {
router.HandleFunc("/api/v1/disks", am.ViewAccess(aH.getDisks)).Methods(http.MethodGet) router.HandleFunc("/api/v1/disks", am.ViewAccess(aH.getDisks)).Methods(http.MethodGet)
// === Preference APIs ===
// user actions
router.HandleFunc("/api/v1/user/preferences", am.ViewAccess(aH.getAllUserPreferences)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/user/preferences/{preferenceId}", am.ViewAccess(aH.getUserPreference)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/user/preferences/{preferenceId}", am.ViewAccess(aH.updateUserPreference)).Methods(http.MethodPut)
// org actions
router.HandleFunc("/api/v1/org/preferences", am.AdminAccess(aH.getAllOrgPreferences)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/org/preferences/{preferenceId}", am.AdminAccess(aH.getOrgPreference)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/org/preferences/{preferenceId}", am.AdminAccess(aH.updateOrgPreference)).Methods(http.MethodPut)
// === Authentication APIs === // === Authentication APIs ===
router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost) router.HandleFunc("/api/v1/invite", am.AdminAccess(aH.inviteUser)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet) router.HandleFunc("/api/v1/invite/{token}", am.OpenAccess(aH.getInvite)).Methods(http.MethodGet)
@ -1329,8 +1347,44 @@ func (aH *APIHandler) getServiceOverview(w http.ResponseWriter, r *http.Request)
func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Request) { func (aH *APIHandler) getServicesTopLevelOps(w http.ResponseWriter, r *http.Request) {
var start, end time.Time var start, end time.Time
var services []string
result, _, apiErr := aH.reader.GetTopLevelOperations(r.Context(), aH.skipConfig, start, end) type topLevelOpsParams struct {
Service string `json:"service"`
Start string `json:"start"`
End string `json:"end"`
}
var params topLevelOpsParams
err := json.NewDecoder(r.Body).Decode(&params)
if err != nil {
zap.L().Error("Error in getting req body for get top operations API", zap.Error(err))
}
if params.Service != "" {
services = []string{params.Service}
}
startEpoch := params.Start
if startEpoch != "" {
startEpochInt, err := strconv.ParseInt(startEpoch, 10, 64)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading start time")
return
}
start = time.Unix(0, startEpochInt)
}
endEpoch := params.End
if endEpoch != "" {
endEpochInt, err := strconv.ParseInt(endEpoch, 10, 64)
if err != nil {
RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: err}, "Error reading end time")
return
}
end = time.Unix(0, endEpochInt)
}
result, apiErr := aH.reader.GetTopLevelOperations(r.Context(), aH.skipConfig, start, end, services)
if apiErr != nil { if apiErr != nil {
RespondError(w, apiErr, nil) RespondError(w, apiErr, nil)
return return
@ -2192,6 +2246,115 @@ func (aH *APIHandler) WriteJSON(w http.ResponseWriter, r *http.Request, response
w.Write(resp) w.Write(resp)
} }
// Preferences
func (ah *APIHandler) getUserPreference(
w http.ResponseWriter, r *http.Request,
) {
preferenceId := mux.Vars(r)["preferenceId"]
user := common.GetUserFromContext(r.Context())
preference, apiErr := preferences.GetUserPreference(
r.Context(), preferenceId, user.User.OrgId, user.User.Id,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
ah.Respond(w, preference)
}
func (ah *APIHandler) updateUserPreference(
w http.ResponseWriter, r *http.Request,
) {
preferenceId := mux.Vars(r)["preferenceId"]
user := common.GetUserFromContext(r.Context())
req := preferences.UpdatePreference{}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
preference, apiErr := preferences.UpdateUserPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.Id)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
ah.Respond(w, preference)
}
func (ah *APIHandler) getAllUserPreferences(
w http.ResponseWriter, r *http.Request,
) {
user := common.GetUserFromContext(r.Context())
preference, apiErr := preferences.GetAllUserPreferences(
r.Context(), user.User.OrgId, user.User.Id,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
ah.Respond(w, preference)
}
func (ah *APIHandler) getOrgPreference(
w http.ResponseWriter, r *http.Request,
) {
preferenceId := mux.Vars(r)["preferenceId"]
user := common.GetUserFromContext(r.Context())
preference, apiErr := preferences.GetOrgPreference(
r.Context(), preferenceId, user.User.OrgId,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
ah.Respond(w, preference)
}
func (ah *APIHandler) updateOrgPreference(
w http.ResponseWriter, r *http.Request,
) {
preferenceId := mux.Vars(r)["preferenceId"]
req := preferences.UpdatePreference{}
user := common.GetUserFromContext(r.Context())
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
preference, apiErr := preferences.UpdateOrgPreference(r.Context(), preferenceId, req.PreferenceValue, user.User.OrgId)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
ah.Respond(w, preference)
}
func (ah *APIHandler) getAllOrgPreferences(
w http.ResponseWriter, r *http.Request,
) {
user := common.GetUserFromContext(r.Context())
preference, apiErr := preferences.GetAllOrgPreferences(
r.Context(), user.User.OrgId,
)
if apiErr != nil {
RespondError(w, apiErr, nil)
return
}
ah.Respond(w, preference)
}
// Integrations // Integrations
func (ah *APIHandler) RegisterIntegrationRoutes(router *mux.Router, am *AuthMiddleware) { func (ah *APIHandler) RegisterIntegrationRoutes(router *mux.Router, am *AuthMiddleware) {
subRouter := router.PathPrefix("/api/v1/integrations").Subrouter() subRouter := router.PathPrefix("/api/v1/integrations").Subrouter()
@ -3050,6 +3213,22 @@ func (aH *APIHandler) queryRangeV3(ctx context.Context, queryRangeParams *v3.Que
} }
} }
// WARN: Only works for AND operator in traces query
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
if isUsed == true && len(traceIDs) > 0 {
zap.L().Debug("traceID used as filter in traces query")
// query signoz_spans table with traceID to get min and max timestamp
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
if err == nil {
// add timestamp filter to queryRange params
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
zap.L().Debug("post adding timestamp filter in traces query", zap.Any("queryRangeParams", queryRangeParams))
}
}
}
result, errQuriesByName, err = aH.querier.QueryRange(ctx, queryRangeParams, spanKeys) result, errQuriesByName, err = aH.querier.QueryRange(ctx, queryRangeParams, spanKeys)
if err != nil { if err != nil {
@ -3319,6 +3498,22 @@ func (aH *APIHandler) queryRangeV4(ctx context.Context, queryRangeParams *v3.Que
} }
} }
// WARN: Only works for AND operator in traces query
if queryRangeParams.CompositeQuery.QueryType == v3.QueryTypeBuilder {
// check if traceID is used as filter (with equal/similar operator) in traces query if yes add timestamp filter to queryRange params
isUsed, traceIDs := tracesV3.TraceIdFilterUsedWithEqual(queryRangeParams)
if isUsed == true && len(traceIDs) > 0 {
zap.L().Debug("traceID used as filter in traces query")
// query signoz_spans table with traceID to get min and max timestamp
min, max, err := aH.reader.GetMinAndMaxTimestampForTraceID(ctx, traceIDs)
if err == nil {
// add timestamp filter to queryRange params
tracesV3.AddTimestampFilters(min, max, queryRangeParams)
zap.L().Debug("post adding timestamp filter in traces query", zap.Any("queryRangeParams", queryRangeParams))
}
}
}
result, errQuriesByName, err = aH.querierV2.QueryRange(ctx, queryRangeParams, spanKeys) result, errQuriesByName, err = aH.querierV2.QueryRange(ctx, queryRangeParams, spanKeys)
if err != nil { if err != nil {

View File

@ -110,6 +110,13 @@ service:
``` ```
### If using non-default nginx log format, adjust log parsing regex
If you are using a [custom nginx log format](https://docs.nginx.com/nginx/admin-guide/monitoring/logging/#setting-up-the-access-log),
please adjust the regex used for parsing logs in the receivers named
`filelog/nginx-access-logs` and `filelog/nginx-error-logs` in collector config.
#### Set Environment Variables #### Set Environment Variables
Set the following environment variables in your otel-collector environment: Set the following environment variables in your otel-collector environment:

View File

@ -17,6 +17,7 @@ const (
ARRAY_INT64 = "Array(Int64)" ARRAY_INT64 = "Array(Int64)"
ARRAY_FLOAT64 = "Array(Float64)" ARRAY_FLOAT64 = "Array(Float64)"
ARRAY_BOOL = "Array(Bool)" ARRAY_BOOL = "Array(Bool)"
NGRAM_SIZE = 4
) )
var dataTypeMapping = map[string]string{ var dataTypeMapping = map[string]string{
@ -72,6 +73,7 @@ func getPath(keyArr []string) string {
func getJSONFilterKey(key v3.AttributeKey, op v3.FilterOperator, isArray bool) (string, error) { func getJSONFilterKey(key v3.AttributeKey, op v3.FilterOperator, isArray bool) (string, error) {
keyArr := strings.Split(key.Key, ".") keyArr := strings.Split(key.Key, ".")
// i.e it should be at least body.name, and not something like body
if len(keyArr) < 2 { if len(keyArr) < 2 {
return "", fmt.Errorf("incorrect key, should contain at least 2 parts") return "", fmt.Errorf("incorrect key, should contain at least 2 parts")
} }
@ -106,6 +108,29 @@ func getJSONFilterKey(key v3.AttributeKey, op v3.FilterOperator, isArray bool) (
return keyname, nil return keyname, nil
} }
// takes the path and the values and generates where clauses for better usage of index
func getPathIndexFilter(path string) string {
filters := []string{}
keyArr := strings.Split(path, ".")
if len(keyArr) < 2 {
return ""
}
for i, key := range keyArr {
if i == 0 {
continue
}
key = strings.TrimSuffix(key, "[*]")
if len(key) >= NGRAM_SIZE {
filters = append(filters, strings.ToLower(key))
}
}
if len(filters) > 0 {
return fmt.Sprintf("lower(body) like lower('%%%s%%')", strings.Join(filters, "%"))
}
return ""
}
func GetJSONFilter(item v3.FilterItem) (string, error) { func GetJSONFilter(item v3.FilterItem) (string, error) {
dataType := item.Key.DataType dataType := item.Key.DataType
@ -154,11 +179,28 @@ func GetJSONFilter(item v3.FilterItem) (string, error) {
return "", fmt.Errorf("unsupported operator: %s", op) return "", fmt.Errorf("unsupported operator: %s", op)
} }
filters := []string{}
pathFilter := getPathIndexFilter(item.Key.Key)
if pathFilter != "" {
filters = append(filters, pathFilter)
}
if op == v3.FilterOperatorContains ||
op == v3.FilterOperatorEqual ||
op == v3.FilterOperatorHas {
val, ok := item.Value.(string)
if ok && len(val) >= NGRAM_SIZE {
filters = append(filters, fmt.Sprintf("lower(body) like lower('%%%s%%')", utils.QuoteEscapedString(strings.ToLower(val))))
}
}
// add exists check for non array items as default values of int/float/bool will corrupt the results // add exists check for non array items as default values of int/float/bool will corrupt the results
if !isArray && !(item.Operator == v3.FilterOperatorExists || item.Operator == v3.FilterOperatorNotExists) { if !isArray && !(item.Operator == v3.FilterOperatorExists || item.Operator == v3.FilterOperatorNotExists) {
existsFilter := fmt.Sprintf("JSON_EXISTS(body, '$.%s')", getPath(strings.Split(item.Key.Key, ".")[1:])) existsFilter := fmt.Sprintf("JSON_EXISTS(body, '$.%s')", getPath(strings.Split(item.Key.Key, ".")[1:]))
filter = fmt.Sprintf("%s AND %s", existsFilter, filter) filter = fmt.Sprintf("%s AND %s", existsFilter, filter)
} }
return filter, nil filters = append(filters, filter)
return strings.Join(filters, " AND "), nil
} }

View File

@ -168,7 +168,7 @@ var testGetJSONFilterData = []struct {
Operator: "has", Operator: "has",
Value: "index_service", Value: "index_service",
}, },
Filter: "has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service')", Filter: "lower(body) like lower('%requestor_list%') AND lower(body) like lower('%index_service%') AND has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service')",
}, },
{ {
Name: "Array membership int64", Name: "Array membership int64",
@ -181,7 +181,7 @@ var testGetJSONFilterData = []struct {
Operator: "has", Operator: "has",
Value: 2, Value: 2,
}, },
Filter: "has(JSONExtract(JSON_QUERY(body, '$.\"int_numbers\"[*]'), '" + ARRAY_INT64 + "'), 2)", Filter: "lower(body) like lower('%int_numbers%') AND has(JSONExtract(JSON_QUERY(body, '$.\"int_numbers\"[*]'), '" + ARRAY_INT64 + "'), 2)",
}, },
{ {
Name: "Array membership float64", Name: "Array membership float64",
@ -194,7 +194,7 @@ var testGetJSONFilterData = []struct {
Operator: "nhas", Operator: "nhas",
Value: 2.2, Value: 2.2,
}, },
Filter: "NOT has(JSONExtract(JSON_QUERY(body, '$.\"nested_num\"[*].\"float_nums\"[*]'), '" + ARRAY_FLOAT64 + "'), 2.200000)", Filter: "lower(body) like lower('%nested_num%float_nums%') AND NOT has(JSONExtract(JSON_QUERY(body, '$.\"nested_num\"[*].\"float_nums\"[*]'), '" + ARRAY_FLOAT64 + "'), 2.200000)",
}, },
{ {
Name: "Array membership bool", Name: "Array membership bool",
@ -207,7 +207,7 @@ var testGetJSONFilterData = []struct {
Operator: "has", Operator: "has",
Value: true, Value: true,
}, },
Filter: "has(JSONExtract(JSON_QUERY(body, '$.\"bool\"[*]'), '" + ARRAY_BOOL + "'), true)", Filter: "lower(body) like lower('%bool%') AND has(JSONExtract(JSON_QUERY(body, '$.\"bool\"[*]'), '" + ARRAY_BOOL + "'), true)",
}, },
{ {
Name: "eq operator", Name: "eq operator",
@ -220,7 +220,7 @@ var testGetJSONFilterData = []struct {
Operator: "=", Operator: "=",
Value: "hello", Value: "hello",
}, },
Filter: "JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') = 'hello'", Filter: "lower(body) like lower('%message%') AND lower(body) like lower('%hello%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') = 'hello'",
}, },
{ {
Name: "eq operator number", Name: "eq operator number",
@ -233,7 +233,7 @@ var testGetJSONFilterData = []struct {
Operator: "=", Operator: "=",
Value: 1, Value: 1,
}, },
Filter: "JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') = 1", Filter: "lower(body) like lower('%status%') AND JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') = 1",
}, },
{ {
Name: "neq operator number", Name: "neq operator number",
@ -246,7 +246,7 @@ var testGetJSONFilterData = []struct {
Operator: "=", Operator: "=",
Value: 1.1, Value: 1.1,
}, },
Filter: "JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + FLOAT64 + "') = 1.100000", Filter: "lower(body) like lower('%status%') AND JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + FLOAT64 + "') = 1.100000",
}, },
{ {
Name: "eq operator bool", Name: "eq operator bool",
@ -259,7 +259,7 @@ var testGetJSONFilterData = []struct {
Operator: "=", Operator: "=",
Value: true, Value: true,
}, },
Filter: "JSON_EXISTS(body, '$.\"boolkey\"') AND JSONExtract(JSON_VALUE(body, '$.\"boolkey\"'), '" + BOOL + "') = true", Filter: "lower(body) like lower('%boolkey%') AND JSON_EXISTS(body, '$.\"boolkey\"') AND JSONExtract(JSON_VALUE(body, '$.\"boolkey\"'), '" + BOOL + "') = true",
}, },
{ {
Name: "greater than operator", Name: "greater than operator",
@ -272,7 +272,7 @@ var testGetJSONFilterData = []struct {
Operator: ">", Operator: ">",
Value: 1, Value: 1,
}, },
Filter: "JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') > 1", Filter: "lower(body) like lower('%status%') AND JSON_EXISTS(body, '$.\"status\"') AND JSONExtract(JSON_VALUE(body, '$.\"status\"'), '" + INT64 + "') > 1",
}, },
{ {
Name: "regex operator", Name: "regex operator",
@ -285,7 +285,7 @@ var testGetJSONFilterData = []struct {
Operator: "regex", Operator: "regex",
Value: "a*", Value: "a*",
}, },
Filter: "JSON_EXISTS(body, '$.\"message\"') AND match(JSON_VALUE(body, '$.\"message\"'), 'a*')", Filter: "lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"') AND match(JSON_VALUE(body, '$.\"message\"'), 'a*')",
}, },
{ {
Name: "contains operator", Name: "contains operator",
@ -298,7 +298,7 @@ var testGetJSONFilterData = []struct {
Operator: "contains", Operator: "contains",
Value: "a", Value: "a",
}, },
Filter: "JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%'", Filter: "lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%'",
}, },
{ {
Name: "contains operator with quotes", Name: "contains operator with quotes",
@ -311,7 +311,7 @@ var testGetJSONFilterData = []struct {
Operator: "contains", Operator: "contains",
Value: "hello 'world'", Value: "hello 'world'",
}, },
Filter: "JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%hello \\'world\\'%'", Filter: "lower(body) like lower('%message%') AND lower(body) like lower('%hello \\'world\\'%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%hello \\'world\\'%'",
}, },
{ {
Name: "exists", Name: "exists",
@ -324,7 +324,7 @@ var testGetJSONFilterData = []struct {
Operator: "exists", Operator: "exists",
Value: "", Value: "",
}, },
Filter: "JSON_EXISTS(body, '$.\"message\"')", Filter: "lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"')",
}, },
} }

View File

@ -51,6 +51,8 @@ var logOperators = map[v3.FilterOperator]string{
v3.FilterOperatorNotExists: "not has(%s_%s_key, '%s')", v3.FilterOperatorNotExists: "not has(%s_%s_key, '%s')",
} }
const BODY = "body"
func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string { func getClickhouseLogsColumnType(columnType v3.AttributeKeyType) string {
if columnType == v3.AttributeKeyTypeTag { if columnType == v3.AttributeKeyTypeTag {
return "attributes" return "attributes"
@ -193,10 +195,24 @@ func buildLogsTimeSeriesFilterQuery(fs *v3.FilterSet, groupBy []v3.AttributeKey,
case v3.FilterOperatorContains, v3.FilterOperatorNotContains: case v3.FilterOperatorContains, v3.FilterOperatorNotContains:
columnName := getClickhouseColumnName(item.Key) columnName := getClickhouseColumnName(item.Key)
val := utils.QuoteEscapedString(fmt.Sprintf("%v", item.Value)) val := utils.QuoteEscapedString(fmt.Sprintf("%v", item.Value))
conditions = append(conditions, fmt.Sprintf("%s %s '%%%s%%'", columnName, logsOp, val)) if columnName == BODY {
logsOp = strings.Replace(logsOp, "ILIKE", "LIKE", 1) // removing i from ilike and not ilike
conditions = append(conditions, fmt.Sprintf("lower(%s) %s lower('%%%s%%')", columnName, logsOp, val))
} else {
conditions = append(conditions, fmt.Sprintf("%s %s '%%%s%%'", columnName, logsOp, val))
}
default: default:
columnName := getClickhouseColumnName(item.Key) columnName := getClickhouseColumnName(item.Key)
fmtVal := utils.ClickHouseFormattedValue(value) fmtVal := utils.ClickHouseFormattedValue(value)
// for use lower for like and ilike
if op == v3.FilterOperatorLike || op == v3.FilterOperatorNotLike {
if columnName == BODY {
logsOp = strings.Replace(logsOp, "ILIKE", "LIKE", 1) // removing i from ilike and not ilike
columnName = fmt.Sprintf("lower(%s)", columnName)
fmtVal = fmt.Sprintf("lower(%s)", fmtVal)
}
}
conditions = append(conditions, fmt.Sprintf("%s %s %s", columnName, logsOp, fmtVal)) conditions = append(conditions, fmt.Sprintf("%s %s %s", columnName, logsOp, fmtVal))
} }
} else { } else {
@ -477,7 +493,7 @@ type Options struct {
} }
func isOrderByTs(orderBy []v3.OrderBy) bool { func isOrderByTs(orderBy []v3.OrderBy) bool {
if len(orderBy) == 1 && orderBy[0].Key == constants.TIMESTAMP { if len(orderBy) == 1 && (orderBy[0].Key == constants.TIMESTAMP || orderBy[0].ColumnName == constants.TIMESTAMP) {
return true return true
} }
return false return false

View File

@ -130,6 +130,14 @@ var timeSeriesFilterQueryData = []struct {
}}, }},
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'user_name')] = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'", ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'user_name')] = 'john' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] != 'my_service'",
}, },
{
Name: "Test attribute and resource attribute with different case",
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
{Key: v3.AttributeKey{Key: "user_name", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "%JoHn%", Operator: "like"},
{Key: v3.AttributeKey{Key: "k8s_namespace", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeResource}, Value: "%MyService%", Operator: "nlike"},
}},
ExpectedFilter: "attributes_string_value[indexOf(attributes_string_key, 'user_name')] ILIKE '%JoHn%' AND resources_string_value[indexOf(resources_string_key, 'k8s_namespace')] NOT ILIKE '%MyService%'",
},
{ {
Name: "Test materialized column", Name: "Test materialized column",
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{ FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
@ -287,6 +295,22 @@ var timeSeriesFilterQueryData = []struct {
}}, }},
ExpectedFilter: "`attribute_int64_status_exists`=false", ExpectedFilter: "`attribute_int64_status_exists`=false",
}, },
{
Name: "Test for body contains and ncontains",
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "contains", Value: "test"},
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "ncontains", Value: "test1"},
}},
ExpectedFilter: "lower(body) LIKE lower('%test%') AND lower(body) NOT LIKE lower('%test1%')",
},
{
Name: "Test for body like and nlike",
FilterSet: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "like", Value: "test"},
{Key: v3.AttributeKey{Key: "body", DataType: v3.AttributeKeyDataTypeString, IsColumn: true}, Operator: "nlike", Value: "test1"},
}},
ExpectedFilter: "lower(body) LIKE lower('test') AND lower(body) NOT LIKE lower('test1')",
},
} }
func TestBuildLogsTimeSeriesFilterQuery(t *testing.T) { func TestBuildLogsTimeSeriesFilterQuery(t *testing.T) {
@ -851,7 +875,7 @@ var testBuildLogsQueryData = []struct {
}, },
}, },
TableName: "logs", TableName: "logs",
ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND body ILIKE '%test%' AND has(attributes_string_key, 'name') group by ts having value > 10 order by value DESC", ExpectedQuery: "SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toFloat64(count(distinct(attributes_string_value[indexOf(attributes_string_key, 'name')]))) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND lower(body) LIKE lower('%test%') AND has(attributes_string_key, 'name') group by ts having value > 10 order by value DESC",
}, },
{ {
Name: "Test attribute with same name as top level key", Name: "Test attribute with same name as top level key",
@ -981,7 +1005,7 @@ var testBuildLogsQueryData = []struct {
}, },
}, },
TableName: "logs", TableName: "logs",
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%' AND has(attributes_string_key, 'name') group by `name` order by `name` DESC", ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND lower(body) like lower('%message%') AND JSON_EXISTS(body, '$.\"message\"') AND JSON_VALUE(body, '$.\"message\"') ILIKE '%a%' AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
}, },
{ {
Name: "TABLE: Test count with JSON Filter Array, groupBy, orderBy", Name: "TABLE: Test count with JSON Filter Array, groupBy, orderBy",
@ -1015,7 +1039,7 @@ var testBuildLogsQueryData = []struct {
}, },
}, },
TableName: "logs", TableName: "logs",
ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service') AND has(attributes_string_key, 'name') group by `name` order by `name` DESC", ExpectedQuery: "SELECT now() as ts, attributes_string_value[indexOf(attributes_string_key, 'name')] as `name`, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726210000 AND timestamp <= 1680066458000000000) AND lower(body) like lower('%requestor_list%') AND lower(body) like lower('%index_service%') AND has(JSONExtract(JSON_QUERY(body, '$.\"requestor_list\"[*]'), 'Array(String)'), 'index_service') AND has(attributes_string_key, 'name') group by `name` order by `name` DESC",
}, },
} }
@ -1380,6 +1404,66 @@ var testPrepLogsQueryData = []struct {
ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by value DESC LIMIT 10", ExpectedQuery: "SELECT now() as ts, toFloat64(count(*)) as value from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) order by value DESC LIMIT 10",
Options: Options{}, Options: Options{},
}, },
{
Name: "Ignore offset if order by is timestamp in list queries",
PanelType: v3.PanelTypeList,
Start: 1680066360726,
End: 1680066458000,
BuilderQuery: &v3.BuilderQuery{
QueryName: "A",
StepInterval: 60,
AggregateOperator: v3.AggregateOperatorNoOp,
Expression: "A",
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
{Key: v3.AttributeKey{Key: "id", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeUnspecified, IsColumn: true}, Value: "logid", Operator: "<"},
},
},
OrderBy: []v3.OrderBy{
{
ColumnName: "timestamp",
Order: "DESC",
},
},
Offset: 100,
PageSize: 100,
},
TableName: "logs",
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as " +
"attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as " +
"attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND id < 'logid' order by " +
"timestamp DESC LIMIT 100",
},
{
Name: "Don't ignore offset if order by is not timestamp",
PanelType: v3.PanelTypeList,
Start: 1680066360726,
End: 1680066458000,
BuilderQuery: &v3.BuilderQuery{
QueryName: "A",
StepInterval: 60,
AggregateOperator: v3.AggregateOperatorNoOp,
Expression: "A",
Filters: &v3.FilterSet{Operator: "AND", Items: []v3.FilterItem{
{Key: v3.AttributeKey{Key: "method", DataType: v3.AttributeKeyDataTypeString, Type: v3.AttributeKeyTypeTag}, Value: "GET", Operator: "="},
},
},
OrderBy: []v3.OrderBy{
{
ColumnName: "mycolumn",
Order: "DESC",
},
},
Offset: 100,
PageSize: 100,
},
TableName: "logs",
ExpectedQuery: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, body,CAST((attributes_string_key, attributes_string_value), 'Map(String, String)') as " +
"attributes_string,CAST((attributes_int64_key, attributes_int64_value), 'Map(String, Int64)') as attributes_int64,CAST((attributes_float64_key, attributes_float64_value), 'Map(String, Float64)') as " +
"attributes_float64,CAST((attributes_bool_key, attributes_bool_value), 'Map(String, Bool)') as attributes_bool,CAST((resources_string_key, resources_string_value), 'Map(String, String)') as resources_string " +
"from signoz_logs.distributed_logs where (timestamp >= 1680066360726000000 AND timestamp <= 1680066458000000000) AND attributes_string_value[indexOf(attributes_string_key, 'method')] = 'GET' order by " +
"resources_string_value[indexOf(resources_string_key, 'mycolumn')] DESC LIMIT 100 OFFSET 100",
},
} }
func TestPrepareLogsQuery(t *testing.T) { func TestPrepareLogsQuery(t *testing.T) {

Some files were not shown because too many files have changed in this diff Show More