Compare commits

..

30 Commits

Author SHA1 Message Date
nityanandagohain
38b1d92252 fix: revert domain fixes 2025-06-12 22:38:16 +05:30
primus-bot[bot]
17f48d656d chore(release): bump to v0.87.0 (#8222)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-06-11 12:06:17 +05:30
Srikanth Chekuri
2d6774da68 fix: add missing denominator for reset case (#8180) 2025-06-11 11:32:50 +05:30
Vibhu Pandey
62a9d7e602 docs(contributing): add endpoint docs (#8215)
* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs

* docs(contributing): add endpoint docs
2025-06-10 17:25:07 +00:00
Vikrant Gupta
3a2c7a7a68 fix(dashboard): create dashboard panic for id (#8214) 2025-06-10 21:31:56 +05:30
Sahil Khan
33e70d1f37 fix: traces back button issue (#8041)
Co-authored-by: Vishal Sharma <makeavish786@gmail.com>
2025-06-10 13:25:59 +00:00
Srikanth Chekuri
85f04e4bae chore: add querier HTTP API endpoint and bucket cache implementation (#8178)
* chore: update types
1. add partial bool to indicate if the value covers the partial interval
2. add optional unit if present (ex: duration_nano, metrics with units)
3. use pointers wherever necessary
4. add format options for request and remove redundant name in query envelope

* chore: fix some gaps
1. make the range as [start, end)
2. provide the logs statement builder with the body column
3. skip the body filter on resource filter statement builder
4. remove unnecessary agg expr rewriter in metrics
5. add ability to skip full text in where clause visitor

* chore: add API endpoint for new query range

* chore: add bucket cache implementation

* chore: add fingerprinting impl and add bucket cache to querier

* chore: add provider factory
2025-06-10 12:56:28 +00:00
Shaheer Kochai
53f9e7d811 chore: trace funnels bugfixes/improvements (#8114)
* fix: refetch funnel steps overview on clicking refresh

* chore: temporarily hide latency pointer from funnel steps

* chore: remove the existing filters of a step on clicking replace button

* fix(useLocalStorage): stabilize initialValue handling to prevent unnecessary re-renders

* chore: remove p99_latency references from funnel metrics

* fix(useFunnelMetrics): ensure latency type defaults to P99 when undefined
2025-06-10 12:45:25 +00:00
Vibhu Pandey
ad46e22561 docs(contributing): add provider docs (#8193) 2025-06-10 05:39:14 +00:00
Yunus M
e79195ccf1 fix: handle alert list updates (#8109) 2025-06-10 10:56:45 +05:30
Amlan Kumar Nandy
f77bb888a8 chore: add analytics for metrics explorer (#8108) 2025-06-10 04:42:49 +00:00
Amlan Kumar Nandy
baa15baea9 chore: metrics explorer summary view fixes (#8126) 2025-06-10 04:18:12 +00:00
Vibhu Pandey
316e6821f1 feat(statsreporter): report stats on stop (#8187) 2025-06-10 07:55:32 +05:30
Vibhu Pandey
a1fa2769e4 feat(statsreporter): build a statsreporter service (#8177)
- build a new statsreporter service
2025-06-09 16:43:29 +05:30
Vikrant Gupta
decb660992 chore(sqlmigration): drop the rule history and data migrations table (#8181) 2025-06-09 15:46:22 +05:30
Amlan Kumar Nandy
0acbcf8322 chore: remove critters-webpack-plugin (#8172) 2025-06-07 16:21:06 +07:00
dependabot[bot]
11eabdc2ac chore(deps): bump webpack-dev-server from 4.15.2 to 5.2.1 in /frontend (#8160)
* chore(deps): bump webpack-dev-server from 4.15.2 to 5.2.1 in /frontend

Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.15.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* feat: upgraded webpack-cli for compatibility fix for webpack-dev-server upgrade

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
Co-authored-by: SagarRajput-7 <sagar@signoz.io>
2025-06-07 07:24:18 +00:00
Vibhu Pandey
eb94554f5a feat(preference): add support for objects and arrays (#8142)
* refactor(preference): better readability

* refactor: better readability

* refactor: better readability

* fix: change frontend contract

* refactor: change frontend

* refactor: change frontend

* refactor: change frontend

* refactor: change frontend

* chore: fix tsc

* chore: fix tsc

* chore: fix tsc

* chore: fix tsc
2025-06-06 22:38:28 +05:30
Piyush Singariya
e8280dbea4 feat: Adding ContainerInsights in ECS Integrations (AWS) (#8122)
* fix: adding ECS ContainerInsights

* chore: dashboard added

* chore: format integrations.json

* feat(7294): added _dot metrics for aws ecs

---------

Co-authored-by: aniket <aniket@signoz.io>
2025-06-06 09:27:35 +00:00
Nityananda Gohain
44ea237039 fix: remove whitespace from sso cert (#8141)
* fix: remove whitespace from sso cert

* fix: use trimspace instead

* fix: use replaceall
2025-06-06 09:03:46 +00:00
Srikanth Chekuri
72b0214d1d chore: add range query impl for promql (#8130) 2025-06-05 19:18:44 +00:00
Srikanth Chekuri
386a215324 chore: metric statement builder (#8104) 2025-06-06 00:38:48 +05:30
Vibhu Pandey
ba0ba4bbc9 build(go): upgrade purego to v0.8.4 (#8159) 2025-06-05 12:31:49 +00:00
SagarRajput-7
d60c9ab36b feat: handle unkown metric in panel query (#8083)
* feat: handle unkown metric in panel query

* feat: added handling with type empty and key present or not

* feat: added test cases

* feat: added comment to better explain the logic

* feat: fixed operator list for unkown metric

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-06-05 11:23:25 +00:00
Piyush Singariya
90770b90bd feat: Introducing EKS integration (AWS) (#8021)
* feat: introducing EKS integration (AWS)

* fix: update metrics and enable logs collection

* feat: eks Overview dashboard ready

* feat: containerinsights incoming

* chore: dashboard name update

* feat(7294): added _dot metrics for aws ecs

* feat(7274): added dot metrics for overview metrics in eks

* Update pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/overview_dot.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update pkg/query-service/app/cloudintegrations/services/definitions/aws/eks/assets/dashboards/containerinsights_dot.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

---------

Co-authored-by: aniket <aniket@signoz.io>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-06-05 15:35:39 +05:30
Vikrant Gupta
a19874c1dd fix(dashboard): dashboards/alerts info telemetry fix (#8161) 2025-06-05 13:47:25 +05:30
Piyush Singariya
65ff460d63 fix: Enhance filter support for Pipeline Simulation (#8134)
* feat: enhance filter support for JSON log body

* test: added tests for exists and not exists

* test: remove the value
2025-06-05 05:05:39 +00:00
primus-bot[bot]
b9d542a294 chore(release): bump to v0.86.2 (#8154) 2025-06-04 14:53:32 +00:00
aniketio-ctrl
e75e5bdbdb feat(7294): added flag columns in query (#8153)
Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2025-06-04 20:10:52 +05:30
Srikanth Chekuri
0d03203977 chore: add formula evaluator (#8112) 2025-06-04 13:40:42 +00:00
230 changed files with 26007 additions and 2095 deletions

View File

@@ -74,7 +74,8 @@ jobs:
-X github.com/SigNoz/signoz/pkg/version.variant=community
-X github.com/SigNoz/signoz/pkg/version.hash=${{ needs.prepare.outputs.hash }}
-X github.com/SigNoz/signoz/pkg/version.time=${{ needs.prepare.outputs.time }}
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}'
-X github.com/SigNoz/signoz/pkg/version.branch=${{ needs.prepare.outputs.branch }}
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./pkg/query-service/Dockerfile.multi-arch

View File

@@ -108,7 +108,8 @@ jobs:
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1'
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch

View File

@@ -107,7 +107,8 @@ jobs:
-X github.com/SigNoz/signoz/ee/zeus.url=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.staging.signoz.cloud
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1'
-X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.staging.signoz.cloud/api/v1
-X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr'
GO_CGO_ENABLED: 1
DOCKER_BASE_IMAGES: '{"alpine": "alpine:3.20.3"}'
DOCKER_DOCKERFILE_PATH: ./ee/query-service/Dockerfile.multi-arch

View File

@@ -165,12 +165,6 @@ alertmanager:
# Retention of the notification logs.
retention: 120h
##################### Analytics #####################
analytics:
# Whether to enable analytics.
enabled: false
##################### Emailing #####################
emailing:
# Whether to enable emailing.
@@ -215,3 +209,18 @@ sharder:
single:
# The org id to which this instance belongs to.
org_id: org_id
##################### Analytics #####################
analytics:
# Whether to enable analytics.
enabled: false
segment:
# The key to use for segment.
key: ""
##################### StatsReporter #####################
statsreporter:
# Whether to enable stats reporter. This is used to provide valuable insights to the SigNoz team. It does not collect any sensitive/PII data.
enabled: true
# The interval at which the stats are collected.
interval: 6h

View File

@@ -174,7 +174,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.86.1
image: signoz/signoz:v0.87.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.86.1
image: signoz/signoz:v0.87.0
command:
- --config=/root/config/prometheus.yml
ports:

View File

@@ -177,7 +177,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.86.1}
image: signoz/signoz:${VERSION:-v0.87.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -110,7 +110,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.86.1}
image: signoz/signoz:${VERSION:-v0.87.0}
container_name: signoz
command:
- --config=/root/config/prometheus.yml

View File

@@ -0,0 +1,51 @@
# Endpoint
This guide outlines the recommended approach for designing endpoints, with a focus on entity relationships, RESTful structure, and examples from the codebase.
## How do we design an endpoint?
### Understand the core entities and their relationships
Start with understanding the core entities and their relationships. For example:
- **Organization**: an organization can have multiple users
### Structure Endpoints RESTfully
Endpoints should reflect the resource hierarchy and follow RESTful conventions. Use clear, **pluralized resource names** and versioning. For example:
- `POST /v1/organizations` — Create an organization
- `GET /v1/organizations/:id` — Get an organization by id
- `DELETE /v1/organizations/:id` — Delete an organization by id
- `PUT /v1/organizations/:id` — Update an organization by id
- `GET /v1/organizations/:id/users` — Get all users in an organization
- `GET /v1/organizations/me/users` — Get all users in my organization
Think in terms of resource navigation in a file system. For example, to find your organization, you would navigate to the root of the file system and then to the `organizations` directory. To find a user in an organization, you would navigate to the `organizations` directory and then to the `id` directory.
```bash
v1/
├── organizations/
│ └── 123/
│ └── users/
```
`me` endpoints are special. They are used to determine the actual id via some auth/external mechanism. For `me` endpoints, think of the `me` directory being symlinked to your organization directory. For example, if you are a part of the organization `123`, the `me` directory will be symlinked to `/v1/organizations/123`:
```bash
v1/
├── organizations/
│ └── me/ -> symlink to /v1/organizations/123
│ └── users/
│ └── 123/
│ └── users/
```
> 💡 **Note**: There are various ways to structure endpoints. Some prefer to use singular resource names instead of `me`. Others prefer to use singular resource names for all endpoints. We have, however, chosen to standardize our endpoints in the manner described above.
## What should I remember?
- Use clear, **plural resource names**
- Use `me` endpoints for determining the actual id via some auth mechanism
> 💡 **Note**: When in doubt, diagram the relationships and walk through the user flows as if navigating a file system. This will help you design endpoints that are both logical and user-friendly.

View File

@@ -0,0 +1,106 @@
# Provider
SigNoz is built on the provider pattern, a design approach where code is organized into providers that handle specific application responsibilities. Providers act as adapter components that integrate with external services and deliver required functionality to the application.
> 💡 **Note**: Coming from a DDD background? Providers are similar (not exactly the same) to adapter/infrastructure services.
## How to create a new provider?
To create a new provider, create a directory in the `pkg/` directory named after your provider. The provider package consists of four key components:
- **Interface** (`pkg/<name>/<name>.go`): Defines the provider's interface. Other packages should import this interface to use the provider.
- **Config** (`pkg/<name>/config.go`): Contains provider configuration, implementing the `factory.Config` interface from [factory/config.go](/pkg/factory/config.go).
- **Implementation** (`pkg/<name>/<implname><name>/provider.go`): Contains the provider implementation, including a `NewProvider` function that returns a `factory.Provider` interface from [factory/provider.go](/pkg/factory/provider.go).
- **Mock** (`pkg/<name>/<name>test.go`): Provides mocks for the provider, typically used by dependent packages for unit testing.
For example, the [prometheus](/pkg/prometheus) provider delivers a prometheus engine to the application:
- `pkg/prometheus/prometheus.go` - Interface definition
- `pkg/prometheus/config.go` - Configuration
- `pkg/prometheus/clickhouseprometheus/provider.go` - Clickhouse-powered implementation
- `pkg/prometheus/prometheustest/provider.go` - Mock implementation
## How to wire it up?
The `pkg/signoz` package contains the inversion of control container responsible for wiring providers. It handles instantiation, configuration, and assembly of providers based on configuration metadata.
> 💡 **Note**: Coming from a Java background? Providers are similar to Spring beans.
Wiring up a provider involves three steps:
1. Wiring up the configuration
Add your config from `pkg/<name>/config.go` to the `pkg/signoz/config.Config` struct and in new factories:
```go
type Config struct {
...
MyProvider myprovider.Config `mapstructure:"myprovider"`
...
}
func NewConfig(ctx context.Context, resolverConfig config.ResolverConfig, ....) (Config, error) {
...
configFactories := []factory.ConfigFactory{
myprovider.NewConfigFactory(),
}
...
}
```
2. Wiring up the provider
Add available provider implementations in `pkg/signoz/provider.go`:
```go
func NewMyProviderFactories() factory.NamedMap[factory.ProviderFactory[myprovider.MyProvider, myprovider.Config]] {
return factory.MustNewNamedMap(
myproviderone.NewFactory(),
myprovidertwo.NewFactory(),
)
}
```
3. Instantiate the provider by adding it to the `SigNoz` struct in `pkg/signoz/signoz.go`:
```go
type SigNoz struct {
...
MyProvider myprovider.MyProvider
...
}
func New(...) (*SigNoz, error) {
...
myprovider, err := myproviderone.New(ctx, settings, config.MyProvider, "one/two")
if err != nil {
return nil, err
}
...
}
```
## How to use it?
To use a provider, import its interface. For example, to use the prometheus provider, import `pkg/prometheus/prometheus.go`:
```go
import "github.com/SigNoz/signoz/pkg/prometheus/prometheus"
func CreateSomething(ctx context.Context, prometheus prometheus.Prometheus) {
...
prometheus.DoSomething()
...
}
```
## Why do we need this?
Like any dependency injection framework, providers decouple the codebase from implementation details. This is especially valuable in SigNoz's large codebase, where we need to swap implementations without changing dependent code. The provider pattern offers several benefits apart from the obvious one of decoupling:
- Configuration is **defined with each provider and centralized in one place**, making it easier to understand and manage through various methods (environment variables, config files, etc.)
- Provider mocking is **straightforward for unit testing**, with a consistent pattern for locating mocks
- **Multiple implementations** of the same provider are **supported**, as demonstrated by our sqlstore provider
## What should I remember?
- Use the provider pattern wherever applicable.
- Always create a provider **irrespective of the number of implementations**. This makes it easier to add new implementations in the future.

View File

@@ -211,3 +211,16 @@ func (provider *provider) GetFeatureFlags(ctx context.Context, organizationID va
return license.Features, nil
}
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
activeLicense, err := provider.GetActive(ctx, orgID)
if err != nil {
if errors.Ast(err, errors.TypeNotFound) {
return map[string]any{}, nil
}
return nil, err
}
return licensetypes.NewStatsFromLicense(activeLicense), nil
}

View File

@@ -39,6 +39,7 @@ builds:
- -X github.com/SigNoz/signoz/ee/zeus.deprecatedURL=https://license.signoz.io
- -X github.com/SigNoz/signoz/ee/query-service/constants.ZeusURL=https://api.signoz.cloud
- -X github.com/SigNoz/signoz/ee/query-service/constants.LicenseSignozIo=https://license.signoz.io/api/v1
- -X github.com/SigNoz/signoz/pkg/analytics.key=9kRrJ7oPCGPEJLF6QjMPLt5bljFhRQBr
- >-
{{- if eq .Os "linux" }}-linkmode external -extldflags '-static'{{- end }}
mod_timestamp: "{{ .CommitTimestamp }}"

View File

@@ -12,6 +12,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/apis/fields"
"github.com/SigNoz/signoz/pkg/http/middleware"
querierAPI "github.com/SigNoz/signoz/pkg/querier"
baseapp "github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/query-service/app/cloudintegrations"
"github.com/SigNoz/signoz/pkg/query-service/app/integrations"
@@ -58,8 +59,9 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
FluxInterval: opts.FluxInterval,
AlertmanagerAPI: alertmanager.NewAPI(signoz.Alertmanager),
LicensingAPI: httplicensing.NewLicensingAPI(signoz.Licensing),
FieldsAPI: fields.NewAPI(signoz.TelemetryStore, signoz.Instrumentation.Logger()),
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
Signoz: signoz,
QuerierAPI: querierAPI.NewAPI(signoz.Querier),
})
if err != nil {

View File

@@ -294,6 +294,7 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
apiHandler.RegisterQueryRangeV3Routes(r, am)
apiHandler.RegisterInfraMetricsRoutes(r, am)
apiHandler.RegisterQueryRangeV4Routes(r, am)
apiHandler.RegisterQueryRangeV5Routes(r, am)
apiHandler.RegisterWebSocketPaths(r, am)
apiHandler.RegisterMessagingQueuesRoutes(r, am)
apiHandler.RegisterThirdPartyApiRoutes(r, am)

View File

@@ -134,7 +134,7 @@
"uuid": "^8.3.2",
"web-vitals": "^0.2.4",
"webpack": "5.94.0",
"webpack-dev-server": "^4.15.2",
"webpack-dev-server": "^5.2.1",
"webpack-retry-chunk-load-plugin": "3.1.1",
"xstate": "^4.31.0"
},
@@ -197,7 +197,6 @@
"babel-plugin-styled-components": "^1.12.0",
"compression-webpack-plugin": "9.0.0",
"copy-webpack-plugin": "^11.0.0",
"critters-webpack-plugin": "^3.0.1",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.4",
@@ -235,7 +234,7 @@
"ts-node": "^10.2.1",
"typescript-plugin-css-modules": "5.0.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2"
"webpack-cli": "^5.1.4"
},
"lint-staged": {
"*.(js|jsx|ts|tsx)": [

View File

@@ -95,7 +95,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
usersData.data
) {
const isOnboardingComplete = orgPreferences?.find(
(preference: Record<string, any>) => preference.key === 'ORG_ONBOARDING',
(preference: Record<string, any>) => preference.name === 'org_onboarding',
)?.value;
const isFirstUser = checkFirstTimeUser();

View File

@@ -1,18 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllOrgPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllOrgPreferences = async (): Promise<
SuccessResponse<GetAllOrgPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/org/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllOrgPreferences;

View File

@@ -1,18 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetAllUserPreferencesResponseProps } from 'types/api/preferences/userOrgPreferences';
const getAllUserPreferences = async (): Promise<
SuccessResponse<GetAllUserPreferencesResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getAllUserPreferences;

View File

@@ -1,20 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetOrgPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getOrgPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<SuccessResponse<GetOrgPreferenceResponseProps> | ErrorResponse> => {
const response = await axios.get(`/org/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getOrgPreference;

View File

@@ -1,22 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { GetUserPreferenceResponseProps } from 'types/api/preferences/userOrgPreferences';
const getUserPreference = async ({
preferenceID,
}: {
preferenceID: string;
}): Promise<
SuccessResponse<GetUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.get(`/user/preferences/${preferenceID}`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
};
export default getUserPreference;

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateOrgPreferenceProps,
UpdateOrgPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateOrgPreference = async (
preferencePayload: UpdateOrgPreferenceProps,
): Promise<
SuccessResponse<UpdateOrgPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(
`/org/preferences/${preferencePayload.preferenceID}`,
{
preference_value: preferencePayload.value,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateOrgPreference;

View File

@@ -1,28 +0,0 @@
import axios from 'api';
import { ErrorResponse, SuccessResponse } from 'types/api';
import {
UpdateUserPreferenceProps,
UpdateUserPreferenceResponseProps,
} from 'types/api/preferences/userOrgPreferences';
const updateUserPreference = async (
preferencePayload: UpdateUserPreferenceProps,
): Promise<
SuccessResponse<UpdateUserPreferenceResponseProps> | ErrorResponse
> => {
const response = await axios.put(
`/user/preferences/${preferencePayload.preferenceID}`,
{
preference_value: preferencePayload.value,
},
);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data.data,
};
};
export default updateUserPreference;

View File

@@ -196,8 +196,6 @@ export interface FunnelOverviewResponse {
avg_rate: number;
conversion_rate: number | null;
errors: number;
// TODO(shaheer): remove p99_latency once we have support for latency
p99_latency: number;
latency: number;
};
}>;

View File

@@ -0,0 +1,23 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { OrgPreference } from 'types/api/preferences/preference';
const listPreference = async (): Promise<
SuccessResponseV2<OrgPreference[]>
> => {
try {
const response = await axios.get<PayloadProps>(`/org/preferences`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default listPreference;

View File

@@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/preferences/get';
import { OrgPreference } from 'types/api/preferences/preference';
const getPreference = async (
props: Props,
): Promise<SuccessResponseV2<OrgPreference>> => {
try {
const response = await axios.get<PayloadProps>(
`/org/preferences/${props.name}`,
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPreference;

View File

@@ -0,0 +1,22 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/org/preferences/${props.name}`, {
value: props.value,
});
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

@@ -1,24 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/user/getUserPreference';
const getPreference = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const response = await axios.get(`/user/preferences`);
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default getPreference;

View File

@@ -0,0 +1,21 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps } from 'types/api/preferences/list';
import { UserPreference } from 'types/api/preferences/preference';
const list = async (): Promise<SuccessResponseV2<UserPreference[]>> => {
try {
const response = await axios.get<PayloadProps>(`/user/preferences`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default list;

View File

@@ -0,0 +1,25 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/preferences/get';
import { UserPreference } from 'types/api/preferences/preference';
const get = async (
props: Props,
): Promise<SuccessResponseV2<UserPreference>> => {
try {
const response = await axios.get<PayloadProps>(
`/user/preferences/${props.name}`,
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default get;

View File

@@ -0,0 +1,22 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Props } from 'types/api/preferences/update';
const update = async (props: Props): Promise<SuccessResponseV2<null>> => {
try {
const response = await axios.put(`/user/preferences/${props.name}`, {
value: props.value,
});
return {
httpStatusCode: response.status,
data: null,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default update;

View File

@@ -425,3 +425,79 @@ export const metricsEmptyTimeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [];
export const metricsUnknownTimeAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.COUNT,
label: 'Count',
},
{
value: MetricAggregateOperator.RATE,
label: 'Rate',
},
{
value: MetricAggregateOperator.INCREASE,
label: 'Increase',
},
];
export const metricsUnknownSpaceAggregateOperatorOptions: SelectOption<
string,
string
>[] = [
{
value: MetricAggregateOperator.SUM,
label: 'Sum',
},
{
value: MetricAggregateOperator.AVG,
label: 'Avg',
},
{
value: MetricAggregateOperator.MIN,
label: 'Min',
},
{
value: MetricAggregateOperator.MAX,
label: 'Max',
},
{
value: MetricAggregateOperator.P50,
label: 'P50',
},
{
value: MetricAggregateOperator.P75,
label: 'P75',
},
{
value: MetricAggregateOperator.P90,
label: 'P90',
},
{
value: MetricAggregateOperator.P95,
label: 'P95',
},
{
value: MetricAggregateOperator.P99,
label: 'P99',
},
];

View File

@@ -7,7 +7,7 @@ import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import { useCallback, useState } from 'react';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath } from 'react-router-dom';
import { Channels } from 'types/api/channels/getAll';
@@ -17,7 +17,6 @@ import Delete from './Delete';
function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
const { t } = useTranslation(['channels']);
const { notifications } = useNotifications();
const [channels, setChannels] = useState<Channels[]>(allChannels);
const { user } = useAppContext();
const [action] = useComponentPermission(['new_alert_action'], user.role);
@@ -56,14 +55,19 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
<Button onClick={(): void => onClickEditHandler(id)} type="link">
{t('column_channel_edit')}
</Button>
<Delete id={id} setChannels={setChannels} notifications={notifications} />
<Delete id={id} notifications={notifications} />
</>
),
});
}
return (
<ResizeTable columns={columns} dataSource={channels} rowKey="id" bordered />
<ResizeTable
columns={columns}
dataSource={allChannels}
rowKey="id"
bordered
/>
);
}

View File

@@ -1,14 +1,15 @@
import { Button } from 'antd';
import { NotificationInstance } from 'antd/es/notification/interface';
import deleteChannel from 'api/channels/delete';
import { Dispatch, SetStateAction, useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Channels } from 'types/api/channels/getAll';
import { useQueryClient } from 'react-query';
import APIError from 'types/api/error';
function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
function Delete({ notifications, id }: DeleteProps): JSX.Element {
const { t } = useTranslation(['channels']);
const [loading, setLoading] = useState(false);
const queryClient = useQueryClient();
const onClickHandler = async (): Promise<void> => {
try {
@@ -21,7 +22,8 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
message: 'Success',
description: t('channel_delete_success'),
});
setChannels((preChannels) => preChannels.filter((e) => e.id !== id));
// Invalidate and refetch
queryClient.invalidateQueries(['getChannels']);
setLoading(false);
} catch (error) {
notifications.error({
@@ -46,7 +48,6 @@ function Delete({ notifications, setChannels, id }: DeleteProps): JSX.Element {
interface DeleteProps {
notifications: NotificationInstance;
setChannels: Dispatch<SetStateAction<Channels[]>>;
id: string;
}

View File

@@ -24,6 +24,10 @@ import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
import {
MetricsExplorerEventKeys,
MetricsExplorerEvents,
} from 'container/MetricsExplorer/events';
import { useOptionsMenu } from 'container/OptionsMenu';
import {
defaultLogsSelectedColumns,
@@ -140,7 +144,9 @@ function ExplorerOptions({
panelType,
});
} else if (isMetricsExplorer) {
logEvent('Metrics Explorer: Save view clicked', {
logEvent(MetricsExplorerEvents.SaveViewClicked, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
panelType,
});
}
@@ -184,8 +190,10 @@ function ExplorerOptions({
panelType,
});
} else if (isMetricsExplorer) {
logEvent('Metrics Explorer: Create alert', {
logEvent(MetricsExplorerEvents.AddToAlertClicked, {
panelType,
[MetricsExplorerEventKeys.Tab]: 'explorer',
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
});
}
@@ -218,11 +226,14 @@ function ExplorerOptions({
panelType,
});
} else if (isMetricsExplorer) {
logEvent('Metrics Explorer: Add to dashboard clicked', {
logEvent(MetricsExplorerEvents.AddToDashboardClicked, {
panelType,
[MetricsExplorerEventKeys.Tab]: 'explorer',
[MetricsExplorerEventKeys.OneChartPerQueryEnabled]: isOneChartPerQuery,
});
}
setIsExport(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLogsExplorer, isMetricsExplorer, panelType, setIsExport, sourcepage]);
const {

View File

@@ -6,8 +6,8 @@ import { Alert, Button, Popover } from 'antd';
import logEvent from 'api/common/logEvent';
import { HostListPayload } from 'api/infraMonitoring/getHostLists';
import { K8sPodsListPayload } from 'api/infraMonitoring/getK8sPodsList';
import getAllUserPreferences from 'api/preferences/getAllUserPreference';
import updateUserPreferenceAPI from 'api/preferences/updateUserPreference';
import listUserPreferences from 'api/v1/user/preferences/list';
import updateUserPreferenceAPI from 'api/v1/user/preferences/name/update';
import Header from 'components/Header/Header';
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
import { FeatureKeys } from 'constants/features';
@@ -29,8 +29,8 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation, useQuery } from 'react-query';
import { UserPreference } from 'types/api/preferences/preference';
import { DataSource } from 'types/common/queryBuilder';
import { UserPreference } from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -185,7 +185,7 @@ export default function Home(): JSX.Element {
const processUserPreferences = (userPreferences: UserPreference[]): void => {
const checklistSkipped = userPreferences?.find(
(preference) => preference.key === 'WELCOME_CHECKLIST_DO_LATER',
(preference) => preference.name === 'welcome_checklist_do_later',
)?.value;
const updatedChecklistItems = cloneDeep(checklistItems);
@@ -194,7 +194,7 @@ export default function Home(): JSX.Element {
const newItem = { ...item };
newItem.isSkipped =
userPreferences?.find(
(preference) => preference.key === item.skippedPreferenceKey,
(preference) => preference.name === item.skippedPreferenceKey,
)?.value || false;
return newItem;
});
@@ -206,13 +206,13 @@ export default function Home(): JSX.Element {
// Fetch User Preferences
const { refetch: refetchUserPreferences } = useQuery({
queryFn: () => getAllUserPreferences(),
queryFn: () => listUserPreferences(),
queryKey: ['getUserPreferences'],
enabled: true,
refetchOnWindowFocus: false,
onSuccess: (response) => {
if (response.payload && response.payload.data) {
processUserPreferences(response.payload.data);
if (response.data) {
processUserPreferences(response.data);
}
setLoadingUserPreferences(false);
@@ -239,7 +239,7 @@ export default function Home(): JSX.Element {
setUpdatingUserPreferences(true);
updateUserPreference({
preferenceID: 'WELCOME_CHECKLIST_DO_LATER',
name: 'welcome_checklist_do_later',
value: true,
});
};
@@ -249,7 +249,7 @@ export default function Home(): JSX.Element {
setUpdatingUserPreferences(true);
updateUserPreference({
preferenceID: item.skippedPreferenceKey,
name: item.skippedPreferenceKey,
value: true,
});
}

View File

@@ -3,15 +3,15 @@ import ROUTES from 'constants/routes';
import { ChecklistItem } from './HomeChecklist/HomeChecklist';
export const checkListStepToPreferenceKeyMap = {
WILL_DO_LATER: 'WELCOME_CHECKLIST_DO_LATER',
SEND_LOGS: 'WELCOME_CHECKLIST_SEND_LOGS_SKIPPED',
SEND_TRACES: 'WELCOME_CHECKLIST_SEND_TRACES_SKIPPED',
SEND_INFRA_METRICS: 'WELCOME_CHECKLIST_SEND_INFRA_METRICS_SKIPPED',
SETUP_DASHBOARDS: 'WELCOME_CHECKLIST_SETUP_DASHBOARDS_SKIPPED',
SETUP_ALERTS: 'WELCOME_CHECKLIST_SETUP_ALERTS_SKIPPED',
SETUP_SAVED_VIEWS: 'WELCOME_CHECKLIST_SETUP_SAVED_VIEW_SKIPPED',
SETUP_WORKSPACE: 'WELCOME_CHECKLIST_SETUP_WORKSPACE_SKIPPED',
ADD_DATA_SOURCE: 'WELCOME_CHECKLIST_ADD_DATA_SOURCE_SKIPPED',
WILL_DO_LATER: 'welcome_checklist_do_later',
SEND_LOGS: 'welcome_checklist_send_logs_skipped',
SEND_TRACES: 'welcome_checklist_send_traces_skipped',
SEND_INFRA_METRICS: 'welcome_checklist_send_infra_metrics_skipped',
SETUP_DASHBOARDS: 'welcome_checklist_setup_dashboards_skipped',
SETUP_ALERTS: 'welcome_checklist_setup_alerts_skipped',
SETUP_SAVED_VIEWS: 'welcome_checklist_setup_saved_view_skipped',
SETUP_WORKSPACE: 'welcome_checklist_setup_workspace_skipped',
ADD_DATA_SOURCE: 'welcome_checklist_add_data_source_skipped',
};
export const DOCS_LINKS = {

View File

@@ -2,6 +2,7 @@ import './Explorer.styles.scss';
import * as Sentry from '@sentry/react';
import { Switch } from 'antd';
import logEvent from 'api/common/logEvent';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
@@ -10,7 +11,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -18,6 +19,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import { v4 as uuid } from 'uuid';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import QuerySection from './QuerySection';
import TimeSeries from './TimeSeries';
import { ExplorerTabs } from './types';
@@ -93,6 +95,12 @@ function Explorer(): JSX.Element {
[stagedQuery],
);
useEffect(() => {
logEvent(MetricsExplorerEvents.TabChanged, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
});
}, []);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-explore-container">

View File

@@ -1,4 +1,5 @@
import { Button } from 'antd';
import logEvent from 'api/common/logEvent';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QueryBuilder } from 'container/QueryBuilder';
import { ButtonWrapper } from 'container/TracesExplorer/QuerySection/styles';
@@ -6,6 +7,8 @@ import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQ
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DataSource } from 'types/common/queryBuilder';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
function QuerySection(): JSX.Element {
const { handleRunQuery } = useQueryBuilder();
@@ -19,7 +22,15 @@ function QuerySection(): JSX.Element {
version="v4"
actions={
<ButtonWrapper>
<Button onClick={(): void => handleRunQuery()} type="primary">
<Button
onClick={(): void => {
handleRunQuery();
logEvent(MetricsExplorerEvents.QueryBuilderQueryChanged, {
[MetricsExplorerEventKeys.Tab]: 'explorer',
});
}}
type="primary"
>
Run Query
</Button>
</ButtonWrapper>

View File

@@ -4,6 +4,7 @@
import { Color } from '@signozhq/design-tokens';
import { Card, Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import logEvent from 'api/common/logEvent';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import classNames from 'classnames';
import ResizeTable from 'components/ResizeTable/ResizeTable';
@@ -11,6 +12,7 @@ import { DataType } from 'container/LogDetailedView/TableView';
import { ArrowDownCircle, ArrowRightCircle, Focus } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import {
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW,
TIME_AGGREGATION_OPTIONS,
@@ -39,6 +41,21 @@ function ExpandedView({
setSelectedTimeSeries,
] = useState<InspectMetricsSeries | null>(null);
useEffect(() => {
logEvent(MetricsExplorerEvents.InspectPointClicked, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
metricInspectionOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
metricInspectionOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
metricInspectionOptions.spaceAggregationLabels,
});
}, [metricInspectionOptions]);
useEffect(() => {
if (step !== InspectionStep.COMPLETED) {
setSelectedTimeSeries(options?.timeSeries ?? null);

View File

@@ -1,5 +1,6 @@
import { Color } from '@signozhq/design-tokens';
import { Button, Skeleton, Switch, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import Uplot from 'components/Uplot';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
@@ -8,6 +9,7 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import { METRIC_TYPE_TO_COLOR_MAP, METRIC_TYPE_TO_ICON_MAP } from './constants';
import GraphPopover from './GraphPopover';
@@ -203,7 +205,14 @@ function GraphView({
<div className="view-toggle-button">
<Switch
checked={viewType === 'graph'}
onChange={(checked): void => setViewType(checked ? 'graph' : 'table')}
onChange={(checked): void => {
const newViewType = checked ? 'graph' : 'table';
setViewType(newViewType);
logEvent(MetricsExplorerEvents.InspectViewChanged, {
[MetricsExplorerEventKeys.Tab]: 'inspect',
[MetricsExplorerEventKeys.InspectView]: newViewType,
});
}}
/>
<Typography.Text>
{viewType === 'graph' ? 'Graph View' : 'Table View'}

View File

@@ -3,6 +3,7 @@ import './Inspect.styles.scss';
import * as Sentry from '@sentry/react';
import { Color } from '@signozhq/design-tokens';
import { Button, Drawer, Empty, Skeleton, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
@@ -11,11 +12,16 @@ import { Compass } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import ExpandedView from './ExpandedView';
import GraphView from './GraphView';
import QueryBuilder from './QueryBuilder';
import Stepper from './Stepper';
import { GraphPopoverOptions, InspectProps } from './types';
import {
GraphPopoverOptions,
InspectProps,
MetricInspectionAction,
} from './types';
import { useInspectMetrics } from './useInspectMetrics';
function Inspect({
@@ -92,6 +98,25 @@ function Inspect({
reset,
} = useInspectMetrics(metricName);
const handleDispatchMetricInspectionOptions = useCallback(
(action: MetricInspectionAction): void => {
dispatchMetricInspectionOptions(action);
logEvent(MetricsExplorerEvents.InspectQueryChanged, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
metricInspectionOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
metricInspectionOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
metricInspectionOptions.spaceAggregationLabels,
});
},
[dispatchMetricInspectionOptions, metricInspectionOptions],
);
const selectedMetricType = useMemo(
() => metricDetailsData?.payload?.data?.metadata?.metric_type,
[metricDetailsData],
@@ -186,7 +211,7 @@ function Inspect({
setMetricName={setMetricName}
spaceAggregationLabels={spaceAggregationLabels}
metricInspectionOptions={metricInspectionOptions}
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
dispatchMetricInspectionOptions={handleDispatchMetricInspectionOptions}
inspectionStep={inspectionStep}
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
searchQuery={searchQuery}
@@ -227,12 +252,18 @@ function Inspect({
popoverOptions,
metricInspectionOptions,
spaceAggregationLabels,
dispatchMetricInspectionOptions,
handleDispatchMetricInspectionOptions,
searchQuery,
expandedViewOptions,
timeAggregatedSeriesMap,
]);
useEffect(() => {
logEvent(MetricsExplorerEvents.ModalOpened, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
}, []);
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<Drawer

View File

@@ -1,5 +1,6 @@
/* eslint-disable no-nested-ternary */
import { Card, Input, Select, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import classNames from 'classnames';
@@ -16,6 +17,7 @@ import {
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import {
SPACE_AGGREGATION_OPTIONS,
TIME_AGGREGATION_OPTIONS,
@@ -135,6 +137,9 @@ export function MetricFilters({
}}
onChange={(value): void => {
handleChangeQueryData('filters', value);
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: value,

View File

@@ -1,5 +1,6 @@
import { Button, Collapse, Input, Menu, Popover, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import logEvent from 'api/common/logEvent';
import { ResizeTable } from 'components/ResizeTable';
import { DataType } from 'container/LogDetailedView/TableView';
import { useNotifications } from 'hooks/useNotifications';
@@ -10,6 +11,7 @@ import { useCopyToClipboard } from 'react-use';
import { PANEL_TYPES } from '../../../constants/queryBuilder';
import ROUTES from '../../../constants/routes';
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { AllAttributesProps, AllAttributesValueProps } from './types';
import { getMetricDetailsQuery } from './utils';
@@ -135,9 +137,16 @@ function AllAttributes({
},
ROUTES.METRICS_EXPLORER_EXPLORER,
);
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
[MetricsExplorerEventKeys.AttributeKey]: groupBy,
});
},
[metricName, metricType, handleExplorerTabChange],
);
const goToMetricsExploreWithAppliedAttribute = useCallback(
(key: string, value: string) => {
const compositeQuery = getMetricDetailsQuery(metricName, metricType, {
@@ -153,6 +162,13 @@ function AllAttributes({
},
ROUTES.METRICS_EXPLORER_EXPLORER,
);
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
[MetricsExplorerEventKeys.AttributeKey]: key,
[MetricsExplorerEventKeys.AttributeValue]: value,
});
},
[metricName, metricType, handleExplorerTabChange],
);

View File

@@ -1,5 +1,6 @@
import { Button, Collapse, Input, Select, Typography } from 'antd';
import { ColumnsType } from 'antd/es/table';
import logEvent from 'api/common/logEvent';
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
@@ -11,6 +12,7 @@ import { useNotifications } from 'hooks/useNotifications';
import { Edit2, Save, X } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import {
METRIC_TYPE_LABEL_MAP,
METRIC_TYPE_VALUES_MAP,
@@ -170,6 +172,11 @@ function Metadata({
{
onSuccess: (response): void => {
if (response?.statusCode === 200) {
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
});
notifications.success({
message: 'Metadata updated successfully',
});

View File

@@ -11,14 +11,16 @@ import {
Tooltip,
Typography,
} from 'antd';
import logEvent from 'api/common/logEvent';
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Compass, Crosshair, X } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { PANEL_TYPES } from '../../../constants/queryBuilder';
import ROUTES from '../../../constants/routes';
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { isInspectEnabled } from '../Inspect/utils';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import AllAttributes from './AllAttributes';
@@ -95,11 +97,22 @@ function MetricDetails({
},
ROUTES.METRICS_EXPLORER_EXPLORER,
);
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.Modal]: 'metric-details',
});
}
}, [metricName, handleExplorerTabChange, metric?.metadata?.metric_type]);
const isMetricDetailsError = metricDetailsError || !metric;
useEffect(() => {
logEvent(MetricsExplorerEvents.ModalOpened, {
[MetricsExplorerEventKeys.Modal]: 'metric-details',
});
}, []);
return (
<Drawer
width="60%"

View File

@@ -10,24 +10,21 @@ import {
} from 'antd';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetMetricsListFilterValues } from 'hooks/metricsExplorer/useGetMetricsListFilterValues';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Search } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { COMPOSITE_QUERY_KEY } from './constants';
import { SUMMARY_FILTERS_KEY } from './constants';
function MetricNameSearch(): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
const [, setSearchParams] = useSearchParams();
function MetricNameSearch({
queryFilters,
}: {
queryFilters: TagFilter;
}): JSX.Element {
const [searchParams, setSearchParams] = useSearchParams();
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const [searchString, setSearchString] = useState<string>('');
@@ -70,9 +67,9 @@ function MetricNameSearch(): JSX.Element {
const handleSelect = useCallback(
(selectedMetricName: string): void => {
const newFilter = {
const newFilters = {
items: [
...currentQuery.builder.queryData[0].filters.items,
...queryFilters.items,
{
id: 'metric_name',
op: 'CONTAINS',
@@ -84,27 +81,15 @@ function MetricNameSearch(): JSX.Element {
value: selectedMetricName,
},
],
op: 'AND',
op: 'and',
};
const compositeQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: newFilter,
},
],
},
};
handleChangeQueryData('filters', newFilter);
setSearchParams({
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
});
setIsPopoverOpen(false);
},
[currentQuery, handleChangeQueryData, setSearchParams],
[queryFilters.items, setSearchParams, searchParams],
);
const metricNameFilterValues = useMemo(
@@ -198,7 +183,7 @@ function MetricNameSearch(): JSX.Element {
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>): void => {
const value = e.target.value.trim().toLowerCase();
const value = e.target.value.trim();
setSearchString(value);
debouncedUpdate(value);
},

View File

@@ -1,26 +1,23 @@
import { Button, Menu, Popover, Tooltip } from 'antd';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Search } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import {
COMPOSITE_QUERY_KEY,
METRIC_TYPE_LABEL_MAP,
METRIC_TYPE_VALUES_MAP,
SUMMARY_FILTERS_KEY,
} from './constants';
function MetricTypeSearch(): JSX.Element {
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
function MetricTypeSearch({
queryFilters,
}: {
queryFilters: TagFilter;
}): JSX.Element {
const [searchParams, setSearchParams] = useSearchParams();
const [, setSearchParams] = useSearchParams();
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const menuItems = useMemo(
@@ -40,9 +37,9 @@ function MetricTypeSearch(): JSX.Element {
const handleSelect = useCallback(
(selectedMetricType: string): void => {
if (selectedMetricType !== 'all') {
const newFilter = {
const newFilters = {
items: [
...currentQuery.builder.queryData[0].filters.items,
...queryFilters.items,
{
id: 'metric_type',
op: '=',
@@ -56,49 +53,23 @@ function MetricTypeSearch(): JSX.Element {
],
op: 'AND',
};
const compositeQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: newFilter,
},
],
},
};
handleChangeQueryData('filters', newFilter);
setSearchParams({
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
});
} else {
const newFilter = {
items: currentQuery.builder.queryData[0].filters.items.filter(
(item) => item.id !== 'metric_type',
),
const newFilters = {
items: queryFilters.items.filter((item) => item.id !== 'metric_type'),
op: 'AND',
};
const compositeQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: newFilter,
},
],
},
};
handleChangeQueryData('filters', newFilter);
setSearchParams({
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(newFilters),
});
}
setIsPopoverOpen(false);
},
[currentQuery, handleChangeQueryData, setSearchParams],
[queryFilters.items, setSearchParams, searchParams],
);
const menu = (

View File

@@ -12,7 +12,7 @@ import { Info } from 'lucide-react';
import { useCallback } from 'react';
import { MetricsListItemRowData, MetricsTableProps } from './types';
import { metricsTableColumns } from './utils';
import { getMetricsTableColumns } from './utils';
function MetricsTable({
isLoading,
@@ -24,6 +24,7 @@ function MetricsTable({
setOrderBy,
totalCount,
openMetricDetails,
queryFilters,
}: MetricsTableProps): JSX.Element {
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
(
@@ -74,7 +75,7 @@ function MetricsTable({
),
}}
dataSource={data}
columns={metricsTableColumns}
columns={getMetricsTableColumns(queryFilters)}
locale={{
emptyText: isLoading ? null : (
<div
@@ -107,7 +108,7 @@ function MetricsTable({
total: totalCount,
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => openMetricDetails(record.key),
onClick: (): void => openMetricDetails(record.key, 'list'),
className: 'clickable-row',
})}
/>

View File

@@ -154,7 +154,7 @@ function MetricsTreemap({
<foreignObject
width={nodeWidth}
height={nodeHeight}
onClick={(): void => openMetricDetails(node.data.id)}
onClick={(): void => openMetricDetails(node.data.id, 'treemap')}
>
<div
style={{

View File

@@ -1,29 +1,27 @@
import './Summary.styles.scss';
import * as Sentry from '@sentry/react';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import logEvent from 'api/common/logEvent';
import { initialQueriesMap } from 'constants/queryBuilder';
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetMetricsTreeMap } from 'hooks/metricsExplorer/useGetMetricsTreeMap';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { AppState } from 'store/reducers';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import InspectModal from '../Inspect';
import MetricDetails from '../MetricDetails';
import {
COMPOSITE_QUERY_KEY,
IS_INSPECT_MODAL_OPEN_KEY,
IS_METRIC_DETAILS_OPEN_KEY,
SELECTED_METRIC_NAME_KEY,
SUMMARY_FILTERS_KEY,
} from './constants';
import MetricsSearch from './MetricsSearch';
import MetricsTable from './MetricsTable';
@@ -63,57 +61,37 @@ function Summary(): JSX.Element {
(state) => state.globalTime,
);
const { currentQuery, updateAllQueriesOperators } = useQueryBuilder();
const defaultQuery = useMemo(() => {
const query = updateAllQueriesOperators(
initialQueriesMap.metrics,
PANEL_TYPES.LIST,
DataSource.METRICS,
);
const queryFilters: TagFilter = useMemo(() => {
const encodedFilters = searchParams.get(SUMMARY_FILTERS_KEY);
if (encodedFilters) {
return JSON.parse(encodedFilters);
}
return {
...query,
builder: {
...query.builder,
queryData: [
{
...query.builder.queryData[0],
orderBy: [DEFAULT_ORDER_BY],
},
],
},
items: [],
op: 'AND',
};
}, [updateAllQueriesOperators]);
}, [searchParams]);
useShareBuilderUrl(defaultQuery);
useEffect(() => {
logEvent(MetricsExplorerEvents.TabChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
});
}, []);
useEffect(() => {
logEvent(MetricsExplorerEvents.TimeUpdated, {
[MetricsExplorerEventKeys.Tab]: 'summary',
});
}, [maxTime, minTime]);
// This is used to avoid the filters from being serialized with the id
const currentQueryFiltersString = useMemo(() => {
const filters = currentQuery?.builder?.queryData[0]?.filters;
if (!filters) return '';
const queryFiltersWithoutId = useMemo(() => {
const filtersWithoutId = {
...filters,
items: filters.items.map(({ id, ...rest }) => rest),
...queryFilters,
items: queryFilters.items.map(({ id, ...rest }) => rest),
};
return JSON.stringify(filtersWithoutId);
}, [currentQuery]);
const queryFilters = useMemo(
() =>
currentQuery?.builder?.queryData[0]?.filters || {
items: [],
op: 'and',
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[currentQueryFiltersString],
);
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: currentQuery.builder.queryData[0],
entityVersion: '',
});
}, [queryFilters]);
const metricsListQuery = useMemo(() => {
const baseQuery = getMetricsListQuery();
@@ -146,6 +124,15 @@ function Summary(): JSX.Element {
isError: isMetricsError,
} = useGetMetricsList(metricsListQuery, {
enabled: !!metricsListQuery && !isInspectModalOpen,
queryKey: [
'metricsList',
queryFiltersWithoutId,
orderBy,
pageSize,
currentPage,
minTime,
maxTime,
],
});
const isListViewError = useMemo(
@@ -160,6 +147,13 @@ function Summary(): JSX.Element {
isError: isTreeMapError,
} = useGetMetricsTreeMap(metricsTreemapQuery, {
enabled: !!metricsTreemapQuery && !isInspectModalOpen,
queryKey: [
'metricsTreemap',
queryFiltersWithoutId,
heatmapView,
minTime,
maxTime,
],
});
const isProportionViewError = useMemo(
@@ -169,51 +163,37 @@ function Summary(): JSX.Element {
const handleFilterChange = useCallback(
(value: TagFilter) => {
handleChangeQueryData('filters', value);
const compositeQuery = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
filters: value,
},
],
},
};
setSearchParams({
[COMPOSITE_QUERY_KEY]: JSON.stringify(compositeQuery),
...Object.fromEntries(searchParams.entries()),
[SUMMARY_FILTERS_KEY]: JSON.stringify(value),
});
setCurrentPage(1);
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Tab]: 'summary',
});
},
[handleChangeQueryData, currentQuery, setSearchParams],
[setSearchParams, searchParams],
);
const updatedCurrentQuery = useMemo(
const searchQuery = useMemo(
() => ({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
{
...currentQuery.builder.queryData[0],
aggregateOperator: 'noop',
aggregateAttribute: {
...currentQuery.builder.queryData[0].aggregateAttribute,
},
},
],
},
...initialQueriesMap.metrics.builder.queryData[0],
filters: queryFilters,
}),
[currentQuery],
[queryFilters],
);
const searchQuery = updatedCurrentQuery?.builder?.queryData[0] || null;
const onPaginationChange = (page: number, pageSize: number): void => {
setCurrentPage(page);
setPageSize(pageSize);
logEvent(MetricsExplorerEvents.PageNumberChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.PageNumber]: page,
});
logEvent(MetricsExplorerEvents.PageSizeChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.PageSize]: pageSize,
});
};
const formattedMetricsData = useMemo(
@@ -221,19 +201,28 @@ function Summary(): JSX.Element {
[metricsData],
);
const openMetricDetails = (metricName: string): void => {
const openMetricDetails = (
metricName: string,
view: 'list' | 'treemap',
): void => {
setSelectedMetricName(metricName);
setIsMetricDetailsOpen(true);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[IS_METRIC_DETAILS_OPEN_KEY]: 'true',
[SELECTED_METRIC_NAME_KEY]: metricName,
});
logEvent(MetricsExplorerEvents.MetricClicked, {
[MetricsExplorerEventKeys.MetricName]: metricName,
[MetricsExplorerEventKeys.View]: view,
});
};
const closeMetricDetails = (): void => {
setSelectedMetricName(null);
setIsMetricDetailsOpen(false);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[IS_METRIC_DETAILS_OPEN_KEY]: 'false',
[SELECTED_METRIC_NAME_KEY]: '',
});
@@ -244,24 +233,39 @@ function Summary(): JSX.Element {
setIsInspectModalOpen(true);
setIsMetricDetailsOpen(false);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[IS_INSPECT_MODAL_OPEN_KEY]: 'true',
[SELECTED_METRIC_NAME_KEY]: metricName,
});
};
const closeInspectModal = (): void => {
handleChangeQueryData('filters', {
items: [],
op: 'AND',
});
setIsInspectModalOpen(false);
setSelectedMetricName(null);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
[IS_INSPECT_MODAL_OPEN_KEY]: 'false',
[SELECTED_METRIC_NAME_KEY]: '',
});
};
const handleSetHeatmapView = (view: TreemapViewType): void => {
setHeatmapView(view);
logEvent(MetricsExplorerEvents.TreemapViewChanged, {
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.ViewType]: view,
});
};
const handleSetOrderBy = (orderBy: OrderByPayload): void => {
setOrderBy(orderBy);
logEvent(MetricsExplorerEvents.OrderByApplied, {
[MetricsExplorerEventKeys.Tab]: 'summary',
[MetricsExplorerEventKeys.ColumnName]: orderBy.columnName,
[MetricsExplorerEventKeys.Order]: orderBy.order,
});
};
return (
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
<div className="metrics-explorer-summary-tab">
@@ -272,7 +276,7 @@ function Summary(): JSX.Element {
isError={isProportionViewError}
viewType={heatmapView}
openMetricDetails={openMetricDetails}
setHeatmapView={setHeatmapView}
setHeatmapView={handleSetHeatmapView}
/>
<MetricsTable
isLoading={isMetricsLoading || isMetricsFetching}
@@ -281,9 +285,10 @@ function Summary(): JSX.Element {
pageSize={pageSize}
currentPage={currentPage}
onPaginationChange={onPaginationChange}
setOrderBy={setOrderBy}
setOrderBy={handleSetOrderBy}
totalCount={metricsData?.payload?.data?.total || 0}
openMetricDetails={openMetricDetails}
queryFilters={queryFilters}
/>
</div>
{isMetricDetailsOpen && (

View File

@@ -4,6 +4,7 @@ import * as useQueryBuilderOperationsHooks from 'hooks/queryBuilder/useQueryBuil
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import store from 'store';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import MetricsTable from '../MetricsTable';
import { MetricsListItemRowData } from '../types';
@@ -29,6 +30,11 @@ const mockData: MetricsListItemRowData[] = [
},
];
const mockQueryFilters: TagFilter = {
items: [],
op: 'AND',
};
jest.mock('react-router-dom-v5-compat', () => {
const actual = jest.requireActual('react-router-dom-v5-compat');
return {
@@ -76,6 +82,7 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,
@@ -99,6 +106,7 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
isLoading
/>
</Provider>
@@ -122,6 +130,7 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,
@@ -149,6 +158,7 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,
@@ -177,13 +187,14 @@ describe('MetricsTable', () => {
setOrderBy={jest.fn()}
totalCount={2}
openMetricDetails={mockOpenMetricDetails}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,
);
fireEvent.click(screen.getByText('Metric 1'));
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1');
expect(mockOpenMetricDetails).toHaveBeenCalledWith('metric1', 'list');
});
it('calls setOrderBy when column header is clicked', () => {
@@ -201,6 +212,7 @@ describe('MetricsTable', () => {
setOrderBy={mockSetOrderBy}
totalCount={2}
openMetricDetails={jest.fn()}
queryFilters={mockQueryFilters}
/>
</Provider>
</MemoryRouter>,

View File

@@ -1,45 +1,61 @@
import { Color } from '@signozhq/design-tokens';
import { render } from '@testing-library/react';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { TreemapViewType } from '../types';
import {
formatDataForMetricsTable,
metricsTableColumns,
getMetricsTableColumns,
MetricTypeRenderer,
} from '../utils';
describe('metricsTableColumns', () => {
const mockQueryFilters: TagFilter = {
items: [],
op: 'AND',
};
it('should have correct column definitions', () => {
expect(metricsTableColumns).toHaveLength(6);
expect(getMetricsTableColumns(mockQueryFilters)).toHaveLength(6);
// Metric Name column
expect(metricsTableColumns[0].dataIndex).toBe('metric_name');
expect(metricsTableColumns[0].width).toBe(400);
expect(metricsTableColumns[0].sorter).toBe(false);
expect(getMetricsTableColumns(mockQueryFilters)[0].dataIndex).toBe(
'metric_name',
);
expect(getMetricsTableColumns(mockQueryFilters)[0].width).toBe(400);
expect(getMetricsTableColumns(mockQueryFilters)[0].sorter).toBe(false);
// Description column
expect(metricsTableColumns[1].dataIndex).toBe('description');
expect(metricsTableColumns[1].width).toBe(400);
expect(getMetricsTableColumns(mockQueryFilters)[1].dataIndex).toBe(
'description',
);
expect(getMetricsTableColumns(mockQueryFilters)[1].width).toBe(400);
// Type column
expect(metricsTableColumns[2].dataIndex).toBe('metric_type');
expect(metricsTableColumns[2].width).toBe(150);
expect(metricsTableColumns[2].sorter).toBe(false);
expect(getMetricsTableColumns(mockQueryFilters)[2].dataIndex).toBe(
'metric_type',
);
expect(getMetricsTableColumns(mockQueryFilters)[2].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[2].sorter).toBe(false);
// Unit column
expect(metricsTableColumns[3].dataIndex).toBe('unit');
expect(metricsTableColumns[3].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[3].dataIndex).toBe('unit');
expect(getMetricsTableColumns(mockQueryFilters)[3].width).toBe(150);
// Samples column
expect(metricsTableColumns[4].dataIndex).toBe(TreemapViewType.SAMPLES);
expect(metricsTableColumns[4].width).toBe(150);
expect(metricsTableColumns[4].sorter).toBe(true);
expect(getMetricsTableColumns(mockQueryFilters)[4].dataIndex).toBe(
TreemapViewType.SAMPLES,
);
expect(getMetricsTableColumns(mockQueryFilters)[4].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[4].sorter).toBe(true);
// Time Series column
expect(metricsTableColumns[5].dataIndex).toBe(TreemapViewType.TIMESERIES);
expect(metricsTableColumns[5].width).toBe(150);
expect(metricsTableColumns[5].sorter).toBe(true);
expect(getMetricsTableColumns(mockQueryFilters)[5].dataIndex).toBe(
TreemapViewType.TIMESERIES,
);
expect(getMetricsTableColumns(mockQueryFilters)[5].width).toBe(150);
expect(getMetricsTableColumns(mockQueryFilters)[5].sorter).toBe(true);
});
describe('MetricTypeRenderer', () => {

View File

@@ -36,4 +36,4 @@ export const METRIC_TYPE_VALUES_MAP = {
export const IS_METRIC_DETAILS_OPEN_KEY = 'isMetricDetailsOpen';
export const IS_INSPECT_MODAL_OPEN_KEY = 'isInspectModalOpen';
export const SELECTED_METRIC_NAME_KEY = 'selectedMetricName';
export const COMPOSITE_QUERY_KEY = 'compositeQuery';
export const SUMMARY_FILTERS_KEY = 'summaryFilters';

View File

@@ -1,5 +1,5 @@
import { MetricsTreeMapResponse } from 'api/metricsExplorer/getMetricsTreeMap';
import React, { Dispatch, SetStateAction } from 'react';
import React from 'react';
import {
IBuilderQuery,
TagFilter,
@@ -12,9 +12,10 @@ export interface MetricsTableProps {
pageSize: number;
currentPage: number;
onPaginationChange: (page: number, pageSize: number) => void;
setOrderBy: Dispatch<SetStateAction<OrderByPayload>>;
setOrderBy: (orderBy: OrderByPayload) => void;
totalCount: number;
openMetricDetails: (metricName: string) => void;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
queryFilters: TagFilter;
}
export interface MetricsSearchProps {
@@ -27,7 +28,7 @@ export interface MetricsTreemapProps {
isLoading: boolean;
isError: boolean;
viewType: TreemapViewType;
openMetricDetails: (metricName: string) => void;
openMetricDetails: (metricName: string, view: 'list' | 'treemap') => void;
setHeatmapView: (value: TreemapViewType) => void;
}

View File

@@ -18,18 +18,21 @@ import {
Gauge,
} from 'lucide-react';
import { useMemo } from 'react';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { METRIC_TYPE_LABEL_MAP } from './constants';
import MetricNameSearch from './MetricNameSearch';
import MetricTypeSearch from './MetricTypeSearch';
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
export const getMetricsTableColumns = (
queryFilters: TagFilter,
): ColumnType<MetricsListItemRowData>[] => [
{
title: (
<div className="metric-name-column-header">
<span className="metric-name-column-header-text">METRIC</span>
<MetricNameSearch />
<MetricNameSearch queryFilters={queryFilters} />
</div>
),
dataIndex: 'metric_name',
@@ -51,7 +54,7 @@ export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
title: (
<div className="metric-type-column-header">
<span className="metric-type-column-header-text">TYPE</span>
<MetricTypeSearch />
<MetricTypeSearch queryFilters={queryFilters} />
</div>
),
dataIndex: 'metric_type',

View File

@@ -0,0 +1,51 @@
/**
* This file contains all analytics events for the Metrics Explorer.
*/
export enum MetricsExplorerEvents {
TabChanged = 'Metrics Explorer: Tab visited',
ModalOpened = 'Metrics Explorer: Modal opened',
MetricClicked = 'Metrics Explorer: Metric clicked',
FilterApplied = 'Metrics Explorer: Filter applied',
TimeUpdated = 'Metrics Explorer: Time updated',
TreemapViewChanged = 'Metrics Explorer: Treemap view changed',
PageNumberChanged = 'Metrics Explorer: Page number changed',
PageSizeChanged = 'Metrics Explorer: Page size changed',
OrderByApplied = 'Metrics Explorer: Order by applied',
MetricMetadataUpdated = 'Metrics Explorer: Metric metadata updated',
OpenInExplorerClicked = 'Metrics Explorer: Open in explorer clicked',
InspectViewChanged = 'Metrics Explorer: Inspect view changed',
InspectQueryChanged = 'Metrics Explorer: Inspect query changed',
InspectPointClicked = 'Metrics Explorer: Inspect point clicked',
QueryBuilderQueryChanged = 'Metrics Explorer: QueryBuilder query changed',
YAxisUnitApplied = 'Metrics Explorer: Y axis unit applied',
AddToAlertClicked = 'Metrics Explorer: Add to alert clicked',
AddToDashboardClicked = 'Metrics Explorer: Add to dashboard clicked',
SaveViewClicked = 'Metrics Explorer: Save view clicked',
SearchApplied = 'Metrics Explorer: Search applied',
ViewEdited = 'Metrics Explorer: View edited',
ViewDeleted = 'Metrics Explorer: View deleted',
}
export enum MetricsExplorerEventKeys {
Tab = 'tab',
Modal = 'modal',
View = 'view',
Interval = 'interval',
ViewType = 'viewType',
PageNumber = 'pageNumber',
PageSize = 'pageSize',
ColumnName = 'columnName',
Order = 'order',
AttributeKey = 'attributeKey',
AttributeValue = 'attributeValue',
MetricName = 'metricName',
InspectView = 'inspectView',
TimeAggregationOption = 'timeAggregationOption',
TimeAggregationInterval = 'timeAggregationInterval',
SpaceAggregationOption = 'spaceAggregationOption',
SpaceAggregationLabels = 'spaceAggregationLabels',
OneChartPerQueryEnabled = 'oneChartPerQueryEnabled',
YAxisUnit = 'yAxisUnit',
ViewName = 'viewName',
Filters = 'filters',
}

View File

@@ -3,8 +3,8 @@ import './OnboardingQuestionaire.styles.scss';
import { NotificationInstance } from 'antd/es/notification/interface';
import logEvent from 'api/common/logEvent';
import updateProfileAPI from 'api/onboarding/updateProfile';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import updateOrgPreferenceAPI from 'api/preferences/updateOrgPreference';
import listOrgPreferences from 'api/v1/org/preferences/list';
import updateOrgPreferenceAPI from 'api/v1/org/preferences/name/update';
import { AxiosError } from 'axios';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { FeatureKeys } from 'constants/features';
@@ -108,13 +108,13 @@ function OnboardingQuestionaire(): JSX.Element {
}, []);
const { refetch: refetchOrgPreferences } = useQuery({
queryFn: () => getAllOrgPreferences(),
queryFn: () => listOrgPreferences(),
queryKey: ['getOrgPreferences'],
enabled: false,
refetchOnWindowFocus: false,
onSuccess: (response) => {
if (response.payload && response.payload.data) {
updateOrgPreferences(response.payload.data);
if (response.data) {
updateOrgPreferences(response.data);
}
setUpdatingOrgOnboardingStatus(false);
@@ -196,7 +196,7 @@ function OnboardingQuestionaire(): JSX.Element {
setUpdatingOrgOnboardingStatus(true);
updateOrgPreference({
preferenceID: 'ORG_ONBOARDING',
name: 'org_onboarding',
value: true,
});
};

View File

@@ -42,7 +42,19 @@ function TraceMetadata(props: ITraceMetadataProps): JSX.Element {
<Button className="previous-btn">
<ArrowLeft
size={14}
onClick={(): void => history.push(ROUTES.TRACES_EXPLORER)}
onClick={(): void => {
// Check if page was opened in new tab (no referrer from same origin)
// or if there's no meaningful history to go back to
const hasValidReferrer =
document.referrer &&
new URL(document.referrer).origin === window.location.origin;
if (hasValidReferrer && window.history.length > 1) {
history.goBack();
} else {
history.push(ROUTES.TRACES_EXPLORER);
}
}}
/>
</Button>
<div className="trace-name">

View File

@@ -16,11 +16,51 @@
&-body {
padding: 14px 16px !important;
padding-bottom: 0 !important;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
}
&--details {
.ant-modal-content {
height: 710px;
&-footer {
margin-top: 0;
background: var(--bg-ink-400);
border-top: 1px solid var(--bg-slate-500);
padding: 16px !important;
.add-span-to-funnel-modal {
&__save-button {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
color: var(--bg-vanilla-100);
font-size: 12px;
font-weight: 500;
line-height: 24px;
width: 135px;
.ant-btn-icon {
display: flex;
}
&:disabled {
color: var(--bg-vanilla-400);
.ant-btn-icon {
svg {
stroke: var(--bg-vanilla-400);
}
}
}
}
&__discard-button {
background: var(--bg-slate-500);
}
}
.ant-btn {
border-radius: 2px;
padding: 4px 8px;
margin: 0 !important;
border: none;
box-shadow: none;
}
}
}
}

View File

@@ -9,14 +9,18 @@ import {
useFunnelDetails,
useFunnelsList,
} from 'hooks/TracesFunnels/useFunnels';
import { ArrowLeft, Plus, Search } from 'lucide-react';
import { isEqual } from 'lodash-es';
import { ArrowLeft, Check, Plus, Search } from 'lucide-react';
import FunnelConfiguration from 'pages/TracesFunnelDetails/components/FunnelConfiguration/FunnelConfiguration';
import { TracesFunnelsContentRenderer } from 'pages/TracesFunnels';
import CreateFunnel from 'pages/TracesFunnels/components/CreateFunnel/CreateFunnel';
import { FunnelListItem } from 'pages/TracesFunnels/components/FunnelsList/FunnelsList';
import { FunnelProvider } from 'pages/TracesFunnels/FunnelContext';
import {
FunnelProvider,
useFunnelContext,
} from 'pages/TracesFunnels/FunnelContext';
import { filterFunnelsByQuery } from 'pages/TracesFunnels/utils';
import { ChangeEvent, useMemo, useState } from 'react';
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2';
import { FunnelData } from 'types/api/traceFunnels';
@@ -28,10 +32,35 @@ enum ModalView {
function FunnelDetailsView({
funnel,
span,
triggerAutoSave,
showNotifications,
onChangesDetected,
triggerDiscard,
}: {
funnel: FunnelData;
span: Span;
triggerAutoSave: boolean;
showNotifications: boolean;
onChangesDetected: (hasChanges: boolean) => void;
triggerDiscard: boolean;
}): JSX.Element {
const { handleRestoreSteps, steps } = useFunnelContext();
// Track changes between current steps and original steps
useEffect(() => {
const hasChanges = !isEqual(steps, funnel.steps);
if (onChangesDetected) {
onChangesDetected(hasChanges);
}
}, [steps, funnel.steps, onChangesDetected]);
// Handle discard when triggered from parent
useEffect(() => {
if (triggerDiscard && funnel.steps) {
handleRestoreSteps(funnel.steps);
}
}, [triggerDiscard, funnel.steps, handleRestoreSteps]);
return (
<div className="add-span-to-funnel-modal__details">
<FunnelListItem
@@ -39,10 +68,18 @@ function FunnelDetailsView({
shouldRedirectToTracesListOnDeleteSuccess={false}
isSpanDetailsPage
/>
<FunnelConfiguration funnel={funnel} isTraceDetailsPage span={span} />
<FunnelConfiguration
funnel={funnel}
isTraceDetailsPage
span={span}
disableAutoSave
triggerAutoSave={triggerAutoSave}
showNotifications={showNotifications}
/>
</div>
);
}
interface AddSpanToFunnelModalProps {
isOpen: boolean;
onClose: () => void;
@@ -60,6 +97,9 @@ function AddSpanToFunnelModal({
undefined,
);
const [isCreateModalOpen, setIsCreateModalOpen] = useState<boolean>(false);
const [triggerSave, setTriggerSave] = useState<boolean>(false);
const [isUnsavedChanges, setIsUnsavedChanges] = useState<boolean>(false);
const [triggerDiscard, setTriggerDiscard] = useState<boolean>(false);
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
setSearchQuery(e.target.value);
@@ -92,12 +132,26 @@ function AddSpanToFunnelModal({
const handleBack = (): void => {
setActiveView(ModalView.LIST);
setSelectedFunnelId(undefined);
setIsUnsavedChanges(false);
setTriggerSave(false);
};
const handleCreateNewClick = (): void => {
setIsCreateModalOpen(true);
};
const handleSaveFunnel = (): void => {
setTriggerSave(true);
// Reset trigger after a brief moment to allow the save to be processed
setTimeout(() => setTriggerSave(false), 100);
};
const handleDiscard = (): void => {
setTriggerDiscard(true);
// Reset trigger after a brief moment
setTimeout(() => setTriggerDiscard(false), 100);
};
const renderListView = (): JSX.Element => (
<div className="add-span-to-funnel-modal">
{!!filteredData?.length && (
@@ -156,7 +210,14 @@ function AddSpanToFunnelModal({
<div className="traces-funnel-details__steps-config">
{selectedFunnelId && funnelDetails?.payload && (
<FunnelProvider funnelId={selectedFunnelId}>
<FunnelDetailsView funnel={funnelDetails.payload} span={span} />
<FunnelDetailsView
funnel={funnelDetails.payload}
span={span}
triggerAutoSave={triggerSave}
showNotifications
onChangesDetected={setIsUnsavedChanges}
triggerDiscard={triggerDiscard}
/>
</FunnelProvider>
)}
</div>
@@ -175,18 +236,43 @@ function AddSpanToFunnelModal({
'add-span-to-funnel-modal-container--details':
activeView === ModalView.DETAILS,
})}
okText="Save Funnel"
footer={
activeView === ModalView.LIST && !!filteredData?.length ? (
<Button
type="default"
className="add-span-to-funnel-modal__create-button"
onClick={handleCreateNewClick}
icon={<Plus size={14} />}
>
Create new funnel
</Button>
) : null
activeView === ModalView.DETAILS
? [
<Button key="close" onClick={onClose}>
Close
</Button>,
<Button
type="default"
key="discard"
onClick={handleDiscard}
className="add-span-to-funnel-modal__discard-button"
disabled={!isUnsavedChanges}
>
Discard
</Button>,
<Button
key="save"
type="primary"
className="add-span-to-funnel-modal__save-button"
onClick={handleSaveFunnel}
disabled={!isUnsavedChanges}
icon={<Check size={14} color="var(--bg-vanilla-100)" />}
>
Save Funnel
</Button>,
]
: [
<Button
key="create"
type="default"
className="add-span-to-funnel-modal__create-button"
onClick={handleCreateNewClick}
icon={<Plus size={14} />}
>
Create new funnel
</Button>,
]
}
>
{activeView === ModalView.LIST

View File

@@ -36,8 +36,14 @@ const normalizeSteps = (steps: FunnelStepData[]): FunnelStepData[] => {
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function useFunnelConfiguration({
funnel,
disableAutoSave = false,
triggerAutoSave = false,
showNotifications = false,
}: {
funnel: FunnelData;
disableAutoSave?: boolean;
triggerAutoSave?: boolean;
showNotifications?: boolean;
}): UseFunnelConfiguration {
const { notifications } = useNotifications();
const {
@@ -45,6 +51,7 @@ export default function useFunnelConfiguration({
initialSteps,
hasIncompleteStepFields,
handleRestoreSteps,
handleRunFunnel,
} = useFunnelContext();
// State management
@@ -82,13 +89,23 @@ export default function useFunnelConfiguration({
step.service_name !== nextStep.service_name ||
step.span_name !== nextStep.span_name ||
!isEqual(step.filters, nextStep.filters) ||
step.has_errors !== nextStep.has_errors
step.has_errors !== nextStep.has_errors ||
step.latency_pointer !== nextStep.latency_pointer
);
});
},
[],
);
const hasFunnelLatencyTypeChanged = useCallback(
(prevSteps: FunnelStepData[], nextSteps: FunnelStepData[]): boolean =>
prevSteps.some((step, index) => {
const nextStep = nextSteps[index];
return step.latency_type !== nextStep.latency_type;
}),
[],
);
// Mutation payload preparation
const getUpdatePayload = useCallback(
() => ({
@@ -106,8 +123,20 @@ export default function useFunnelConfiguration({
() => [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnel.funnel_id, selectedTime],
[funnel.funnel_id, selectedTime],
);
// eslint-disable-next-line sonarjs/cognitive-complexity
useEffect(() => {
if (hasStepsChanged() && !hasIncompleteStepFields) {
// Determine if we should save based on the mode
let shouldSave = false;
if (disableAutoSave) {
// Manual save mode: only save when explicitly triggered
shouldSave = triggerAutoSave;
} else {
// Auto-save mode: save when steps have changed and no incomplete fields
shouldSave = hasStepsChanged() && !hasIncompleteStepFields;
}
if (shouldSave && !isEqual(debouncedSteps, lastValidatedSteps)) {
updateStepsMutation.mutate(getUpdatePayload(), {
onSuccess: (data) => {
const updatedFunnelSteps = data?.payload?.steps;
@@ -116,13 +145,16 @@ export default function useFunnelConfiguration({
queryClient.setQueryData(
[REACT_QUERY_KEY.GET_FUNNEL_DETAILS, funnel.funnel_id],
(oldData: any) => ({
...oldData,
payload: {
...oldData.payload,
steps: updatedFunnelSteps,
},
}),
(oldData: any) => {
if (!oldData?.payload) return oldData;
return {
...oldData,
payload: {
...oldData.payload,
steps: updatedFunnelSteps,
},
};
},
);
lastSavedStepsStateRef.current = updatedFunnelSteps;
@@ -131,17 +163,29 @@ export default function useFunnelConfiguration({
(step) => step.service_name === '' || step.span_name === '',
);
// Only validate if service_name or span_name changed
if (
if (hasFunnelLatencyTypeChanged(lastValidatedSteps, debouncedSteps)) {
handleRunFunnel();
setLastValidatedSteps(debouncedSteps);
}
// Only validate if funnel steps definitions
else if (
!hasIncompleteStepFields &&
hasFunnelStepDefinitionsChanged(lastValidatedSteps, debouncedSteps)
) {
queryClient.refetchQueries(validateStepsQueryKey);
setLastValidatedSteps(debouncedSteps);
}
// Show success notification only when requested
if (showNotifications) {
notifications.success({
message: 'Success',
description: 'Funnel configuration updated successfully',
});
}
},
onError: () => {
onError: (error: any) => {
handleRestoreSteps(lastSavedStepsStateRef.current);
queryClient.setQueryData(
[REACT_QUERY_KEY.GET_FUNNEL_DETAILS, funnel.funnel_id],
@@ -153,6 +197,16 @@ export default function useFunnelConfiguration({
},
}),
);
// Show error notification only when requested
if (showNotifications) {
notifications.error({
message: 'Failed to update funnel',
description:
error?.message ||
'An error occurred while updating the funnel configuration',
});
}
},
});
}
@@ -165,6 +219,9 @@ export default function useFunnelConfiguration({
lastValidatedSteps,
queryClient,
validateStepsQueryKey,
triggerAutoSave,
showNotifications,
disableAutoSave,
]);
return {

View File

@@ -49,11 +49,7 @@ export function useFunnelMetrics({
},
{
title: `P99 Latency`,
value: getYAxisFormattedValue(
// TODO(shaheer): remove p99_latency once we have support for latency
(sourceData.latency ?? sourceData.p99_latency).toString(),
'ms',
),
value: getYAxisFormattedValue(sourceData.latency.toString(), 'ms'),
},
];
}, [overviewData?.payload?.data]);
@@ -95,7 +91,10 @@ export function useFunnelStepsMetrics({
} = useFunnelStepsOverview(funnelId, payload);
const latencyType = useMemo(
() => (stepStart ? steps[stepStart]?.latency_type : LatencyOptions.P99),
() =>
stepStart
? steps[stepStart]?.latency_type ?? LatencyOptions.P99
: LatencyOptions.P99,
[stepStart, steps],
);
@@ -117,10 +116,9 @@ export function useFunnelStepsMetrics({
),
},
{
title: `${latencyType?.toUpperCase()} Latency`,
title: `${latencyType.toUpperCase()} Latency`,
value: getYAxisFormattedValue(
// TODO(shaheer): remove p99_latency once we have support for latency
((sourceData.latency ?? sourceData.p99_latency) * 1_000_000).toString(),
(sourceData.latency * 1_000_000).toString(),
'ns',
),
},

View File

@@ -0,0 +1,135 @@
import { act, renderHook } from '@testing-library/react';
import { ENTITY_VERSION_V4 } from 'constants/app';
import { ATTRIBUTE_TYPES } from 'constants/queryBuilder';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { useQueryBuilder } from '../useQueryBuilder';
import { useQueryOperations } from '../useQueryBuilderOperations';
// Mock the useQueryBuilder hook
jest.mock('../useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
describe('useQueryBuilderOperations - Empty Aggregate Attribute Type', () => {
const mockHandleSetQueryData = jest.fn();
const mockHandleSetFormulaData = jest.fn();
const mockRemoveQueryBuilderEntityByIndex = jest.fn();
const mockSetLastUsedQuery = jest.fn();
const mockRedirectWithQueryBuilderData = jest.fn();
const defaultMockQuery: IBuilderQuery = {
dataSource: DataSource.METRICS,
aggregateOperator: MetricAggregateOperator.AVG,
aggregateAttribute: {
key: 'test_metric',
dataType: DataTypes.Float64,
type: ATTRIBUTE_TYPES.GAUGE,
} as BaseAutocompleteData,
timeAggregation: MetricAggregateOperator.AVG,
spaceAggregation: '',
having: [],
limit: null,
queryName: 'test_query',
functions: [],
filters: {
items: [],
op: 'AND',
},
groupBy: [],
orderBy: [],
stepInterval: 60,
expression: '',
disabled: false,
reduceTo: 'avg',
legend: '',
};
const setupMockQueryBuilder = (): void => {
(useQueryBuilder as jest.Mock).mockReturnValue({
handleSetQueryData: mockHandleSetQueryData,
handleSetFormulaData: mockHandleSetFormulaData,
removeQueryBuilderEntityByIndex: mockRemoveQueryBuilderEntityByIndex,
setLastUsedQuery: mockSetLastUsedQuery,
redirectWithQueryBuilderData: mockRedirectWithQueryBuilderData,
panelType: 'time_series',
currentQuery: {
builder: {
queryData: [defaultMockQuery, defaultMockQuery],
},
},
});
};
const renderHookWithProps = (
props = {},
): { current: ReturnType<typeof useQueryOperations> } => {
const { result } = renderHook(() =>
useQueryOperations({
query: defaultMockQuery,
index: 0,
entityVersion: ENTITY_VERSION_V4,
...props,
}),
);
return result;
};
beforeEach(() => {
jest.clearAllMocks();
setupMockQueryBuilder();
});
describe('handleChangeAggregatorAttribute', () => {
it('should set AVG operators when type is empty but key is present - unkown metric', () => {
const result = renderHookWithProps();
const newAttribute: BaseAutocompleteData = {
key: 'new_metric',
dataType: DataTypes.Float64,
type: '',
};
act(() => {
result.current.handleChangeAggregatorAttribute(newAttribute);
});
expect(mockHandleSetQueryData).toHaveBeenCalledWith(
0,
expect.objectContaining({
aggregateAttribute: newAttribute,
aggregateOperator: MetricAggregateOperator.AVG,
timeAggregation: MetricAggregateOperator.AVG,
spaceAggregation: MetricAggregateOperator.AVG,
}),
);
});
it('should set COUNT/RATE/SUM operators when both type and key are empty', () => {
const result = renderHookWithProps();
const newAttribute: BaseAutocompleteData = {
key: '',
dataType: DataTypes.Float64,
type: '',
};
act(() => {
result.current.handleChangeAggregatorAttribute(newAttribute);
});
expect(mockHandleSetQueryData).toHaveBeenCalledWith(
0,
expect.objectContaining({
aggregateAttribute: newAttribute,
aggregateOperator: MetricAggregateOperator.COUNT,
timeAggregation: MetricAggregateOperator.RATE,
spaceAggregation: MetricAggregateOperator.SUM,
}),
);
});
});
});

View File

@@ -12,6 +12,8 @@ import {
metricsGaugeSpaceAggregateOperatorOptions,
metricsHistogramSpaceAggregateOperatorOptions,
metricsSumSpaceAggregateOperatorOptions,
metricsUnknownSpaceAggregateOperatorOptions,
metricsUnknownTimeAggregateOperatorOptions,
} from 'constants/queryBuilderOperators';
import {
listViewInitialLogQuery,
@@ -21,6 +23,7 @@ import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getMetricsOperatorsByAttributeType } from 'lib/newQueryBuilder/getMetricsOperatorsByAttributeType';
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
import { findDataTypeOfOperator } from 'lib/query/findDataTypeOfOperator';
import { isEmpty } from 'lodash-es';
import { useCallback, useEffect, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import {
@@ -145,12 +148,18 @@ export const useQueryOperations: UseQueryOperations = ({
const handleMetricAggregateAtributeTypes = useCallback(
(aggregateAttribute: BaseAutocompleteData): any => {
const newOperators = getMetricsOperatorsByAttributeType({
dataSource: DataSource.METRICS,
panelType: panelType || PANEL_TYPES.TIME_SERIES,
aggregateAttributeType:
(aggregateAttribute.type as ATTRIBUTE_TYPES) || ATTRIBUTE_TYPES.GAUGE,
});
// operators for unknown metric
const isUnknownMetric =
isEmpty(aggregateAttribute.type) && !isEmpty(aggregateAttribute.key);
const newOperators = isUnknownMetric
? metricsUnknownTimeAggregateOperatorOptions
: getMetricsOperatorsByAttributeType({
dataSource: DataSource.METRICS,
panelType: panelType || PANEL_TYPES.TIME_SERIES,
aggregateAttributeType:
(aggregateAttribute.type as ATTRIBUTE_TYPES) || ATTRIBUTE_TYPES.GAUGE,
});
switch (aggregateAttribute.type) {
case ATTRIBUTE_TYPES.SUM:
@@ -168,7 +177,7 @@ export const useQueryOperations: UseQueryOperations = ({
setSpaceAggregationOptions(metricsHistogramSpaceAggregateOperatorOptions);
break;
default:
setSpaceAggregationOptions(metricsGaugeSpaceAggregateOperatorOptions);
setSpaceAggregationOptions(metricsUnknownSpaceAggregateOperatorOptions);
break;
}
@@ -202,6 +211,21 @@ export const useQueryOperations: UseQueryOperations = ({
}
newQuery.spaceAggregation = '';
// Handled query with unknown metric to avoid 400 and 500 errors
// With metric value typed and not available then - time - 'avg', space - 'avg'
// If not typed - time - 'rate', space - 'sum', op - 'count'
if (isEmpty(newQuery.aggregateAttribute.type)) {
if (!isEmpty(newQuery.aggregateAttribute.key)) {
newQuery.aggregateOperator = MetricAggregateOperator.AVG;
newQuery.timeAggregation = MetricAggregateOperator.AVG;
newQuery.spaceAggregation = MetricAggregateOperator.AVG;
} else {
newQuery.aggregateOperator = MetricAggregateOperator.COUNT;
newQuery.timeAggregation = MetricAggregateOperator.RATE;
newQuery.spaceAggregation = MetricAggregateOperator.SUM;
}
}
}
handleSetQueryData(index, newQuery);

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
/**
* A React hook for interacting with localStorage.
@@ -6,35 +6,47 @@ import { useCallback, useEffect, useState } from 'react';
*
* @template T The type of the value to be stored.
* @param {string} key The localStorage key.
* @param {T | (() => T)} initialValue The initial value to use if no value is found in localStorage,
* @param {T | (() => T)} defaultValue The default value to use if no value is found in localStorage,
* @returns {[T, (value: T | ((prevState: T) => T)) => void, () => void]}
* A tuple containing:
* - The current value from state (and localStorage).
* - A function to set the value (updates state and localStorage).
* - A function to remove the value from localStorage and reset state to initialValue.
* - A function to remove the value from localStorage and reset state to defaultValue.
*/
export function useLocalStorage<T>(
key: string,
initialValue: T | (() => T),
defaultValue: T | (() => T),
): [T, (value: T | ((prevState: T) => T)) => void, () => void] {
// This function resolves the initialValue if it's a function,
// Stabilize the defaultValue to prevent unnecessary re-renders
const defaultValueRef = useRef<T | (() => T)>(defaultValue);
// Update the ref if defaultValue changes (for cases where it's intentionally dynamic)
useEffect(() => {
if (defaultValueRef.current !== defaultValue) {
defaultValueRef.current = defaultValue;
}
}, [defaultValue]);
// This function resolves the defaultValue if it's a function,
// and handles potential errors during localStorage access or JSON parsing.
const readValueFromStorage = useCallback((): T => {
const resolvedInitialValue =
initialValue instanceof Function ? initialValue() : initialValue;
const resolveddefaultValue =
defaultValueRef.current instanceof Function
? (defaultValueRef.current as () => T)()
: defaultValueRef.current;
try {
const item = window.localStorage.getItem(key);
// If item exists, parse it, otherwise return the resolved initial value.
// If item exists, parse it, otherwise return the resolved default value.
if (item) {
return JSON.parse(item) as T;
}
} catch (error) {
// Log error and fall back to initial value if reading/parsing fails.
// Log error and fall back to default value if reading/parsing fails.
console.warn(`Error reading localStorage key "${key}":`, error);
}
return resolvedInitialValue;
}, [key, initialValue]);
return resolveddefaultValue;
}, [key]);
// Initialize state by reading from localStorage.
const [storedValue, setStoredValue] = useState<T>(readValueFromStorage);
@@ -63,17 +75,19 @@ export function useLocalStorage<T>(
const removeValue = useCallback(() => {
try {
window.localStorage.removeItem(key);
// Reset state to the (potentially resolved) initialValue.
// Reset state to the (potentially resolved) defaultValue.
setStoredValue(
initialValue instanceof Function ? initialValue() : initialValue,
defaultValueRef.current instanceof Function
? (defaultValueRef.current as () => T)()
: defaultValueRef.current,
);
} catch (error) {
console.warn(`Error removing localStorage key "${key}":`, error);
}
}, [key, initialValue]);
}, [key]);
// useEffect to update the storedValue if the key changes,
// or if the initialValue prop changes causing readValueFromStorage to change.
// or if the defaultValue prop changes causing readValueFromStorage to change.
// This ensures the hook reflects the correct localStorage item if its key prop dynamically changes.
useEffect(() => {
setStoredValue(readValueFromStorage());

View File

@@ -3,7 +3,11 @@ import {
metricsOperatorsByType,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { metricsEmptyTimeAggregateOperatorOptions } from 'constants/queryBuilderOperators';
import {
metricsEmptyTimeAggregateOperatorOptions,
metricsUnknownTimeAggregateOperatorOptions,
} from 'constants/queryBuilderOperators';
import { isEmpty } from 'lodash-es';
import { DataSource } from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
@@ -27,5 +31,9 @@ export const getMetricsOperatorsByAttributeType = ({
}
}
if (dataSource === DataSource.METRICS && isEmpty(aggregateAttributeType)) {
return metricsUnknownTimeAggregateOperatorOptions;
}
return metricsEmptyTimeAggregateOperatorOptions;
};

View File

@@ -6,11 +6,7 @@ import { Filters } from 'components/AlertDetailsFilters/Filters';
import NotFound from 'components/NotFound';
import RouteTab from 'components/RouteTab';
import Spinner from 'components/Spinner';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import history from 'lib/history';
import { useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -74,9 +70,6 @@ BreadCrumbItem.defaultProps = {
function AlertDetails(): JSX.Element {
const { pathname } = useLocation();
const { routes } = useRouteTabUtils();
const urlQuery = useUrlQuery();
const { safeNavigate } = useSafeNavigate();
const { notifications } = useNotifications();
const {
isLoading,
@@ -92,27 +85,6 @@ function AlertDetails(): JSX.Element {
document.title = alertTitle || document.title;
}, [alertDetailsResponse?.payload?.data.alert, isRefetching]);
useEffect(() => {
if (alertDetailsResponse?.payload?.data?.id) {
const ruleUUID = alertDetailsResponse.payload.data.id;
if (ruleId !== ruleUUID) {
urlQuery.set(QueryParams.ruleId, ruleUUID);
const generatedUrl = `${window.location.pathname}?${urlQuery}`;
notifications.info({
message:
"We're transitioning alert rule IDs from integers to UUIDs.Both old and new alert links will continue to work after this change - existing notifications using integer IDs will remain functional while new alerts will use the UUID format. Please use the updated link in the URL for future references",
});
safeNavigate(generatedUrl);
}
}
}, [
alertDetailsResponse?.payload?.data.id,
notifications,
ruleId,
safeNavigate,
urlQuery,
]);
if (
isError ||
!isValidRuleId ||

View File

@@ -2,8 +2,13 @@ import './MetricsExplorerPage.styles.scss';
import RouteTab from 'components/RouteTab';
import { TabRoutes } from 'components/RouteTab/types';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import history from 'lib/history';
import { useMemo } from 'react';
import { useLocation } from 'react-use';
import { DataSource } from 'types/common/queryBuilder';
import { Explorer, Summary, Views } from './constants';
@@ -12,6 +17,20 @@ function MetricsExplorerPage(): JSX.Element {
const routes: TabRoutes[] = [Summary, Explorer, Views];
const { updateAllQueriesOperators } = useQueryBuilder();
const defaultQuery = useMemo(
() =>
updateAllQueriesOperators(
initialQueriesMap[DataSource.METRICS],
PANEL_TYPES.LIST,
DataSource.METRICS,
),
[updateAllQueriesOperators],
);
useShareBuilderUrl(defaultQuery);
return (
<div className="metrics-explorer-page">
<RouteTab routes={routes} activeKey={pathname} history={history} />

View File

@@ -17,6 +17,10 @@ import {
} from 'components/ExplorerCard/utils';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getRandomColor } from 'container/ExplorerOptions/utils';
import {
MetricsExplorerEventKeys,
MetricsExplorerEvents,
} from 'container/MetricsExplorer/events';
import { useDeleteView } from 'hooks/saveViews/useDeleteView';
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
@@ -155,6 +159,10 @@ function SaveView(): JSX.Element {
logEvent('Logs Views: Views visited', {
number: viewsData?.data?.data?.length,
});
} else if (sourcepage === DataSource.METRICS) {
logEvent(MetricsExplorerEvents.TabChanged, {
[MetricsExplorerEventKeys.Tab]: 'views',
});
}
logEventCalledRef.current = true;
}
@@ -176,6 +184,9 @@ function SaveView(): JSX.Element {
});
hideEditViewModal();
refetchAllView();
logEvent(MetricsExplorerEvents.ViewEdited, {
[MetricsExplorerEventKeys.Tab]: 'views',
});
},
onError: (err) => {
showErrorNotification(notifications, err);
@@ -204,6 +215,10 @@ function SaveView(): JSX.Element {
},
SOURCEPAGE_VS_ROUTES[sourcepage],
);
logEvent(MetricsExplorerEvents.OpenInExplorerClicked, {
[MetricsExplorerEventKeys.Tab]: 'views',
[MetricsExplorerEventKeys.ViewName]: name,
});
}
};

View File

@@ -1,6 +1,11 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import logEvent from 'api/common/logEvent';
import { MenuItemLabelGeneratorProps } from 'components/ExplorerCard/types';
import { showErrorNotification } from 'components/ExplorerCard/utils';
import {
MetricsExplorerEventKeys,
MetricsExplorerEvents,
} from 'container/MetricsExplorer/events';
import { UseMutateAsyncFunction } from 'react-query';
import { DeleteViewPayloadProps } from 'types/api/saveViews/types';
@@ -29,6 +34,9 @@ export const deleteViewHandler = ({
message: 'View Deleted Successfully',
});
refetchAllView();
logEvent(MetricsExplorerEvents.ViewDeleted, {
[MetricsExplorerEventKeys.Tab]: 'views',
});
},
onError: (err) => {
showErrorNotification(notifications, err);

View File

@@ -27,7 +27,7 @@ function DeleteFunnelStep({
onCancel={onClose}
rootClassName="funnel-modal delete-funnel-modal"
cancelText="Cancel"
okText="Delete Funnel"
okText="Delete Step"
okButtonProps={{
icon: <Trash2 size={14} />,
type: 'primary',

View File

@@ -2,8 +2,13 @@
&__steps-wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
justify-content: flex-start;
&.funnel-details-page {
height: calc(
100vh - 170px
); // 64px bottom bar + 61px configuration header + 45px page navbar
overflow: auto;
}
}
&__header {
@@ -38,13 +43,26 @@
}
}
}
&__description-wrapper {
padding: 16px 16px 0 16px;
.funnel {
&-title {
color: var(--bg-vanilla-500);
font-size: 16px;
font-weight: 500;
line-height: 24px; /* 150% */
letter-spacing: -0.08px;
}
&-description {
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
}
}
}
&__description {
padding: 16px 16px 0 16px;
color: var(--bg-vanilla-400);
font-size: 12px;
line-height: 18px; /* 150% */
letter-spacing: -0.06px;
}
.funnel-item__action-icon {

View File

@@ -1,6 +1,8 @@
import './FunnelConfiguration.styles.scss';
import { Button, Divider, Tooltip } from 'antd';
import cx from 'classnames';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import useFunnelConfiguration from 'hooks/TracesFunnels/useFunnelConfiguration';
import { PencilLine } from 'lucide-react';
import FunnelItemPopover from 'pages/TracesFunnels/components/FunnelsList/FunnelItemPopover';
@@ -19,15 +21,24 @@ interface FunnelConfigurationProps {
funnel: FunnelData;
isTraceDetailsPage?: boolean;
span?: Span;
disableAutoSave?: boolean;
triggerAutoSave?: boolean;
showNotifications?: boolean;
}
function FunnelConfiguration({
funnel,
isTraceDetailsPage,
span,
disableAutoSave,
triggerAutoSave,
showNotifications,
}: FunnelConfigurationProps): JSX.Element {
const { isPopoverOpen, setIsPopoverOpen, steps } = useFunnelConfiguration({
funnel,
disableAutoSave,
triggerAutoSave,
showNotifications,
});
const [isDescriptionModalOpen, setIsDescriptionModalOpen] = useState<boolean>(
false,
@@ -40,55 +51,69 @@ function FunnelConfiguration({
return (
<div className="funnel-configuration">
{!isTraceDetailsPage && (
<>
<div className="funnel-configuration__header">
<div className="funnel-configuration__header-left">
<FunnelBreadcrumb funnelName={funnel.funnel_name} />
</div>
<div className="funnel-configuration__header-right">
<Tooltip
title={
funnel?.description
? 'Edit funnel description'
: 'Add funnel description'
}
>
<Button
type="text"
className="funnel-item__action-btn funnel-configuration__rename-btn"
icon={<PencilLine size={14} />}
onClick={(): void => setIsDescriptionModalOpen(true)}
aria-label="Edit Funnel Description"
/>
</Tooltip>
<CopyToClipboard textToCopy={window.location.href} />
<Divider type="vertical" />
<FunnelItemPopover
isPopoverOpen={isPopoverOpen}
setIsPopoverOpen={setIsPopoverOpen}
funnel={funnel}
<div className="funnel-configuration__header">
<div className="funnel-configuration__header-left">
<FunnelBreadcrumb funnelName={funnel.funnel_name} />
</div>
<div className="funnel-configuration__header-right">
<Tooltip
title={
funnel?.description
? 'Edit funnel description'
: 'Add funnel description'
}
>
<Button
type="text"
className="funnel-item__action-btn funnel-configuration__rename-btn"
icon={<PencilLine size={14} />}
onClick={(): void => setIsDescriptionModalOpen(true)}
aria-label="Edit Funnel Description"
/>
</div>
</Tooltip>
<CopyToClipboard textToCopy={window.location.href} />
<Divider type="vertical" />
<FunnelItemPopover
isPopoverOpen={isPopoverOpen}
setIsPopoverOpen={setIsPopoverOpen}
funnel={funnel}
/>
</div>
<div className="funnel-configuration__description">
{funnel?.description}
</div>
</>
)}
<div className="funnel-configuration__steps-wrapper">
<div className="funnel-configuration__steps">
{!isTraceDetailsPage && <StepsHeader />}
<StepsContent isTraceDetailsPage={isTraceDetailsPage} span={span} />
</div>
{!isTraceDetailsPage && <StepsFooter stepsCount={steps.length} />}
)}
<div
className={cx('funnel-configuration__steps-wrapper', {
'funnel-details-page': !isTraceDetailsPage,
})}
>
<OverlayScrollbar>
<>
{!isTraceDetailsPage && (
<div className="funnel-configuration__description-wrapper">
<div className="funnel-title">{funnel.funnel_name}</div>
<div className="funnel-description">
{funnel?.description ?? 'No description added.'}
</div>
</div>
)}
<div className="funnel-configuration__steps">
{!isTraceDetailsPage && <StepsHeader />}
<StepsContent isTraceDetailsPage={isTraceDetailsPage} span={span} />
</div>
</>
</OverlayScrollbar>
</div>
{!isTraceDetailsPage && (
<AddFunnelDescriptionModal
isOpen={isDescriptionModalOpen}
onClose={handleDescriptionModalClose}
funnelId={funnel.funnel_id}
funnelDescription={funnel?.description || ''}
/>
<>
<StepsFooter stepsCount={steps.length} />
<AddFunnelDescriptionModal
isOpen={isDescriptionModalOpen}
onClose={handleDescriptionModalClose}
funnelId={funnel.funnel_id}
funnelDescription={funnel?.description || ''}
/>
</>
)}
</div>
);
@@ -97,6 +122,9 @@ function FunnelConfiguration({
FunnelConfiguration.defaultProps = {
isTraceDetailsPage: false,
span: undefined,
disableAutoSave: false,
triggerAutoSave: false,
showNotifications: false,
};
export default memo(FunnelConfiguration);

View File

@@ -33,6 +33,16 @@
display: flex;
flex-direction: column;
gap: 4px;
&__title-container {
display: flex;
align-items: center;
gap: 6px;
.drag-icon {
cursor: grab;
display: flex;
align-items: center;
}
}
&__title {
color: var(--bg-vanilla-400);
font-size: 14px;
@@ -69,14 +79,12 @@
align-items: baseline;
gap: 6px;
padding: 16px;
padding-left: 6px;
padding-left: 12px;
.ant-form-item {
margin: 0;
width: 100%;
}
.drag-icon {
cursor: grab;
}
.filters {
display: flex;
flex-direction: column;

View File

@@ -1,13 +1,11 @@
import './FunnelStep.styles.scss';
import { Button, Divider, Dropdown, Form, Space, Switch, Tooltip } from 'antd';
import { MenuProps } from 'antd/lib';
import { Button, Divider, Form, Switch, Tooltip } from 'antd';
import { FilterSelect } from 'components/CeleryOverview/CeleryOverviewConfigOptions/CeleryOverviewConfigOptions';
import { QueryParams } from 'constants/query';
import { initialQueriesMap } from 'constants/queryBuilder';
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
import { ChevronDown, GripVertical, HardHat, PencilLine } from 'lucide-react';
import { LatencyPointers } from 'pages/TracesFunnelDetails/constants';
import { HardHat, PencilLine } from 'lucide-react';
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
import { useMemo, useState } from 'react';
import { FunnelStepData } from 'types/api/traceFunnels';
@@ -37,16 +35,17 @@ function FunnelStep({
false,
);
const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
(option) => ({
key: option.value,
label: option.key,
style:
option.value === stepData.latency_pointer
? { backgroundColor: 'var(--bg-slate-100)' }
: {},
}),
);
// temporarily hide latency pointer, as it breaks some edge cases (ref: https://signoz-team.slack.com/archives/C089MNX4Y90/p1748600682066499?thread_ts=1748599673.171759&cid=C089MNX4Y90)
// const latencyPointerItems: MenuProps['items'] = LatencyPointers.map(
// (option) => ({
// key: option.value,
// label: option.key,
// style:
// option.value === stepData.latency_pointer
// ? { backgroundColor: 'var(--bg-slate-100)' }
// : {},
// }),
// );
const updatedCurrentQuery = useMemo(
() => ({
@@ -75,11 +74,17 @@ function FunnelStep({
<Form form={form}>
<div className="funnel-step__header">
<div className="funnel-step-details">
{stepData.name ? (
<div className="funnel-step-details__title">{stepData.name}</div>
) : (
<div className="funnel-step-details__title">Step {index + 1}</div>
)}
<div className="funnel-step-details__title-container">
{/* TODO(shaheer): uncomment after adding support for dragging the steps */}
{/* <div className="drag-icon">
<GripVertical size={14} color="var(--bg-slate-200)" />
</div> */}
{stepData.name ? (
<div className="funnel-step-details__title">{stepData.name}</div>
) : (
<div className="funnel-step-details__title">Step {index + 1}</div>
)}
</div>
{!!stepData.description && (
<div className="funnel-step-details__description">
{stepData.description}
@@ -113,9 +118,6 @@ function FunnelStep({
</div>
</div>
<div className="funnel-step__content">
<div className="drag-icon">
<GripVertical size={14} color="var(--bg-slate-200)" />
</div>
<div className="filters">
<div className="filters__service-and-span">
<div className="service">
@@ -176,7 +178,8 @@ function FunnelStep({
/>
<div className="error__label">Errors</div>
</div>
<div className="latency-pointer">
{/* temporarily hide latency pointer, as it breaks some edge cases (ref: https://signoz-team.slack.com/archives/C089MNX4Y90/p1748600682066499?thread_ts=1748599673.171759&cid=C089MNX4Y90) */}
{/* <div className="latency-pointer">
<div className="latency-pointer__label">Latency pointer</div>
<Dropdown
menu={{
@@ -197,7 +200,7 @@ function FunnelStep({
<ChevronDown size={14} color="var(--bg-vanilla-400)" />
</Space>
</Dropdown>
</div>
</div> */}
</div>
</Form>
</div>

View File

@@ -1,8 +1,4 @@
.steps-content {
height: calc(
100vh - 253px
); // 64px (footer) + 12 (steps gap) + 32 (steps header) + 16 (steps padding) + 50 (breadcrumb) + 34 (description) + 45 (steps footer) = 219px
overflow-y: auto;
.ant-btn {
box-shadow: none;
&-icon {

View File

@@ -2,7 +2,6 @@ import './StepsContent.styles.scss';
import { Button, Steps } from 'antd';
import logEvent from 'api/common/logEvent';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { PlusIcon, Undo2 } from 'lucide-react';
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
import { memo, useCallback } from 'react';
@@ -37,70 +36,68 @@ function StepsContent({
return (
<div className="steps-content">
<OverlayScrollbar>
<Steps direction="vertical">
{steps.map((step, index) => (
<Step
key={`step-${index + 1}`}
description={
<div className="steps-content__description">
<div className="funnel-step-wrapper">
<FunnelStep stepData={step} index={index} stepsCount={steps.length} />
{isTraceDetailsPage && span && (
<Button
type="default"
className="funnel-step-wrapper__replace-button"
icon={<Undo2 size={12} />}
disabled={
step.service_name === span.serviceName &&
step.span_name === span.name
}
onClick={(): void =>
handleReplaceStep(index, span.serviceName, span.name)
}
>
Replace
</Button>
)}
</div>
{/* Display InterStepConfig only between steps */}
{index < steps.length - 1 && (
// the latency type should be sent with the n+1th step
<InterStepConfig index={index + 1} step={steps[index + 1]} />
<Steps direction="vertical">
{steps.map((step, index) => (
<Step
key={`step-${index + 1}`}
description={
<div className="steps-content__description">
<div className="funnel-step-wrapper">
<FunnelStep stepData={step} index={index} stepsCount={steps.length} />
{isTraceDetailsPage && span && (
<Button
type="default"
className="funnel-step-wrapper__replace-button"
icon={<Undo2 size={12} />}
disabled={
step.service_name === span.serviceName &&
step.span_name === span.name
}
onClick={(): void =>
handleReplaceStep(index, span.serviceName, span.name)
}
>
Replace
</Button>
)}
</div>
}
/>
))}
{/* For now we are only supporting 3 steps */}
{steps.length < 3 && (
<Step
className="steps-content__add-step"
description={
!isTraceDetailsPage ? (
<Button
type="default"
className="steps-content__add-btn"
onClick={handleAddStep}
icon={<PlusIcon size={14} />}
>
Add Funnel Step
</Button>
) : (
<Button
type="default"
className="steps-content__add-btn"
onClick={handleAddForNewStep}
icon={<PlusIcon size={14} />}
>
Add for new Step
</Button>
)
}
/>
)}
</Steps>
</OverlayScrollbar>
{/* Display InterStepConfig only between steps */}
{index < steps.length - 1 && (
// the latency type should be sent with the n+1th step
<InterStepConfig index={index + 1} step={steps[index + 1]} />
)}
</div>
}
/>
))}
{/* For now we are only supporting 3 steps */}
{steps.length < 3 && (
<Step
className="steps-content__add-step"
description={
!isTraceDetailsPage ? (
<Button
type="default"
className="steps-content__add-btn"
onClick={handleAddStep}
icon={<PlusIcon size={14} />}
>
Add Funnel Step
</Button>
) : (
<Button
type="default"
className="steps-content__add-btn"
onClick={handleAddForNewStep}
icon={<PlusIcon size={14} />}
>
Add for new Step
</Button>
)
}
/>
)}
</Steps>
</div>
);
}

View File

@@ -48,6 +48,13 @@
&--run {
background-color: var(--bg-robin-500);
}
&--updating {
display: flex;
align-items: center;
gap: 4px;
color: var(--bg-vanilla-400);
font-size: 12px;
}
}
}

View File

@@ -1,11 +1,12 @@
import './StepsFooter.styles.scss';
import { Button, Skeleton } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import { Button, Skeleton, Spin } from 'antd';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { Cone, Play, RefreshCcw } from 'lucide-react';
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
import { useEffect, useMemo } from 'react';
import { useIsFetching, useQueryClient } from 'react-query';
import { useMemo } from 'react';
import { useIsFetching, useIsMutating } from 'react-query';
const useFunnelResultsLoading = (): boolean => {
const { funnelId } = useFunnelContext();
@@ -56,25 +57,11 @@ function ValidTracesCount(): JSX.Element {
hasIncompleteStepFields,
validTracesCount,
funnelId,
selectedTime,
} = useFunnelContext();
const queryClient = useQueryClient();
const validationQueryKey = useMemo(
() => [REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS, funnelId, selectedTime],
[funnelId, selectedTime],
);
const validationStatus = queryClient.getQueryData(validationQueryKey);
useEffect(() => {
// Show loading state immediately when fields become valid
if (hasIncompleteStepFields && validationStatus !== 'pending') {
queryClient.setQueryData(validationQueryKey, 'pending');
}
}, [
hasIncompleteStepFields,
queryClient,
validationQueryKey,
validationStatus,
const isFunnelUpdateMutating = useIsMutating([
REACT_QUERY_KEY.UPDATE_FUNNEL_STEPS,
funnelId,
]);
if (hasAllEmptyStepFields) {
@@ -91,7 +78,7 @@ function ValidTracesCount(): JSX.Element {
);
}
if (isValidateStepsLoading || validationStatus === 'pending') {
if (isValidateStepsLoading || isFunnelUpdateMutating) {
return <Skeleton.Button size="small" />;
}
@@ -111,10 +98,16 @@ function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
validTracesCount,
handleRunFunnel,
hasFunnelBeenExecuted,
funnelId,
} = useFunnelContext();
const isFunnelResultsLoading = useFunnelResultsLoading();
const isFunnelUpdateMutating = useIsMutating([
REACT_QUERY_KEY.UPDATE_FUNNEL_STEPS,
funnelId,
]);
return (
<div className="steps-footer">
<div className="steps-footer__left">
@@ -124,6 +117,16 @@ function StepsFooter({ stepsCount }: StepsFooterProps): JSX.Element {
<ValidTracesCount />
</div>
<div className="steps-footer__right">
{!!isFunnelUpdateMutating && (
<div className="steps-footer__button steps-footer__button--updating">
<Spin
indicator={<LoadingOutlined style={{ color: 'grey' }} />}
size="small"
/>
Updating
</div>
)}
{!hasFunnelBeenExecuted ? (
<Button
disabled={validTracesCount === 0}

View File

@@ -3,7 +3,7 @@ import './FunnelResults.styles.scss';
import Spinner from 'components/Spinner';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useFunnelContext } from 'pages/TracesFunnels/FunnelContext';
import { useQueryClient } from 'react-query';
import { useIsMutating } from 'react-query';
import EmptyFunnelResults from './EmptyFunnelResults';
import FunnelGraph from './FunnelGraph';
@@ -18,14 +18,11 @@ function FunnelResults(): JSX.Element {
hasAllEmptyStepFields,
hasFunnelBeenExecuted,
funnelId,
selectedTime,
} = useFunnelContext();
const queryClient = useQueryClient();
const validateQueryData = queryClient.getQueryData([
REACT_QUERY_KEY.VALIDATE_FUNNEL_STEPS,
const isFunnelUpdateMutating = useIsMutating([
REACT_QUERY_KEY.UPDATE_FUNNEL_STEPS,
funnelId,
selectedTime,
]);
if (hasAllEmptyStepFields) return <EmptyFunnelResults />;
@@ -38,7 +35,7 @@ function FunnelResults(): JSX.Element {
/>
);
if (isValidateStepsLoading || validateQueryData === 'pending') {
if (isValidateStepsLoading || isFunnelUpdateMutating) {
return <Spinner size="large" />;
}

View File

@@ -170,6 +170,10 @@ export function FunnelProvider({
handleStepUpdate(index, {
service_name: serviceName,
span_name: spanName,
filters: {
items: [],
op: 'AND',
},
});
logEvent('Trace Funnels: span added (replaced) from trace details page', {});
},
@@ -191,6 +195,11 @@ export function FunnelProvider({
funnelId,
selectedTime,
]);
queryClient.refetchQueries([
REACT_QUERY_KEY.GET_FUNNEL_STEPS_OVERVIEW,
funnelId,
selectedTime,
]);
queryClient.refetchQueries([
REACT_QUERY_KEY.GET_FUNNEL_STEPS_GRAPH_DATA,
funnelId,

View File

@@ -1,6 +1,6 @@
import getLocalStorageApi from 'api/browser/localstorage/get';
import getAllOrgPreferences from 'api/preferences/getAllOrgPreferences';
import { Logout } from 'api/utils';
import listOrgPreferences from 'api/v1/org/preferences/list';
import getUserVersion from 'api/v1/version/getVersion';
import { LOCALSTORAGE } from 'constants/localStorage';
import dayjs from 'dayjs';
@@ -25,8 +25,8 @@ import {
LicenseState,
TrialInfo,
} from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference';
import { Organization } from 'types/api/user/getOrganization';
import { OrgPreference } from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { IAppContext, IUser } from './types';
@@ -147,7 +147,7 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
isFetching: isFetchingOrgPreferences,
error: orgPreferencesFetchError,
} = useQuery({
queryFn: () => getAllOrgPreferences(),
queryFn: () => listOrgPreferences(),
queryKey: ['getOrgPreferences', 'app-context'],
enabled: !!isLoggedIn && !!user.email && user.role === USER_ROLES.ADMIN,
});
@@ -162,9 +162,9 @@ export function AppProvider({ children }: PropsWithChildren): JSX.Element {
if (
!isFetchingOrgPreferences &&
orgPreferencesData &&
orgPreferencesData.payload
orgPreferencesData.data
) {
setOrgPreferences(orgPreferencesData.payload.data);
setOrgPreferences(orgPreferencesData.data);
}
}, [orgPreferencesData, isFetchingOrgPreferences]);

View File

@@ -1,10 +1,10 @@
import APIError from 'types/api/error';
import { FeatureFlagProps as FeatureFlags } from 'types/api/features/getFeaturesFlags';
import { LicenseResModel, TrialInfo } from 'types/api/licensesV3/getActive';
import { OrgPreference } from 'types/api/preferences/preference';
import { Organization } from 'types/api/user/getOrganization';
import { UserResponse as User } from 'types/api/user/getUser';
import { PayloadProps } from 'types/api/user/getVersion';
import { OrgPreference } from 'types/reducer/app';
export interface IAppContext {
user: IUser;

View File

@@ -217,13 +217,11 @@ export function getAppContextMock(
featureFlagsFetchError: null,
orgPreferences: [
{
key: 'ORG_ONBOARDING',
name: 'Organisation Onboarding',
name: 'org_onboarding',
description: 'Organisation Onboarding',
valueType: 'boolean',
defaultValue: false,
allowedValues: [true, false],
isDiscreteValues: true,
allowedValues: ['true', 'false'],
allowedScopes: ['org'],
value: false,
},

View File

@@ -0,0 +1,10 @@
import { OrgPreference, UserPreference } from './preference';
export interface Props {
name: string;
}
export interface PayloadProps {
status: string;
data: OrgPreference | UserPreference;
}

View File

@@ -0,0 +1,6 @@
import { OrgPreference, UserPreference } from './preference';
export interface PayloadProps {
status: string;
data: OrgPreference[] | UserPreference[];
}

View File

@@ -0,0 +1,19 @@
export interface OrgPreference {
name: string;
description: string;
valueType: string;
defaultValue: boolean;
allowedValues: string[];
allowedScopes: string[];
value: boolean;
}
export interface UserPreference {
name: string;
description: string;
valueType: string;
defaultValue: boolean;
allowedValues: string[];
allowedScopes: string[];
value: boolean;
}

View File

@@ -0,0 +1,4 @@
export interface Props {
name: string;
value: unknown;
}

View File

@@ -1,41 +0,0 @@
import { OrgPreference, UserPreference } from 'types/reducer/app';
export interface GetOrgPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface GetUserPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface GetAllOrgPreferencesResponseProps {
status: string;
data: OrgPreference[];
}
export interface GetAllUserPreferencesResponseProps {
status: string;
data: UserPreference[];
}
export interface UpdateOrgPreferenceProps {
preferenceID: string;
value: unknown;
}
export interface UpdateUserPreferenceProps {
preferenceID: string;
value: unknown;
}
export interface UpdateOrgPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}
export interface UpdateUserPreferenceResponseProps {
status: string;
data: Record<string, unknown>;
}

View File

@@ -1,6 +0,0 @@
export interface PayloadProps {
hasOptedUpdates: boolean;
id: number;
isAnonymous: boolean;
uuid: string;
}

View File

@@ -9,30 +9,6 @@ export interface User {
displayName: UserPayload['displayName'];
}
export interface OrgPreference {
key: string;
name: string;
description: string;
valueType: string;
defaultValue: boolean;
allowedValues: any[];
isDiscreteValues: boolean;
allowedScopes: string[];
value: boolean;
}
export interface UserPreference {
key: string;
name: string;
description: string;
valueType: string;
defaultValue: boolean;
allowedValues: any[];
isDiscreteValues: boolean;
allowedScopes: string[];
value: boolean;
}
export default interface AppReducer {
currentVersion: string;
latestVersion: string;

View File

@@ -13,7 +13,6 @@ const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const Critters = require('critters-webpack-plugin');
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin');
dotenv.config();
@@ -62,14 +61,6 @@ const plugins = [
}),
}),
new MiniCssExtractPlugin(),
new Critters({
preload: 'swap',
// Base path location of the CSS files
path: resolve(__dirname, './build/css'),
// Public path of the CSS resources. This prefix is removed from the href
publicPath: resolve(__dirname, './public/css'),
fonts: true,
}),
sentryWebpackPlugin({
authToken: process.env.SENTRY_AUTH_TOKEN,
org: process.env.SENTRY_ORG,

File diff suppressed because it is too large Load Diff

2
go.mod
View File

@@ -104,7 +104,7 @@ require (
github.com/dennwc/varint v1.0.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/elastic/lunes v0.1.0 // indirect
github.com/expr-lang/expr v1.17.0 // indirect

4
go.sum
View File

@@ -207,8 +207,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/elastic/lunes v0.1.0 h1:amRtLPjwkWtzDF/RKzcEPMvSsSseLDLW+bnhfNSLRe4=

View File

@@ -5,6 +5,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -53,4 +54,7 @@ type Alertmanager interface {
// SetDefaultConfig sets the default config for the organization.
SetDefaultConfig(context.Context, string) error
// Collects stats for the organization.
statsreporter.StatsCollector
}

View File

@@ -471,3 +471,12 @@ func (provider *provider) SetDefaultConfig(ctx context.Context, orgID string) er
return provider.configStore.Set(ctx, config)
}
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
channels, err := provider.configStore.ListChannels(ctx, orgID.String())
if err != nil {
return nil, err
}
return alertmanagertypes.NewStatsFromChannels(channels), nil
}

View File

@@ -182,3 +182,12 @@ func (provider *provider) SetDefaultConfig(ctx context.Context, orgID string) er
return provider.configStore.Set(ctx, config)
}
func (provider *provider) Collect(ctx context.Context, orgID valuer.UUID) (map[string]any, error) {
channels, err := provider.configStore.ListChannels(ctx, orgID.String())
if err != nil {
return nil, err
}
return alertmanagertypes.NewStatsFromChannels(channels), nil
}

View File

@@ -0,0 +1,30 @@
package analyticstest
import (
"context"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/types/analyticstypes"
)
var _ analytics.Analytics = (*Provider)(nil)
type Provider struct {
stopC chan struct{}
}
func New() *Provider {
return &Provider{stopC: make(chan struct{})}
}
func (provider *Provider) Start(_ context.Context) error {
<-provider.stopC
return nil
}
func (provider *Provider) Send(ctx context.Context, messages ...analyticstypes.Message) {}
func (provider *Provider) Stop(_ context.Context) error {
close(provider.stopC)
return nil
}

View File

@@ -1,8 +1,6 @@
package analytics
import (
"fmt"
"github.com/SigNoz/signoz/pkg/factory"
)
@@ -12,8 +10,12 @@ var (
)
type Config struct {
Enabled bool `mapstructure:"enabled"`
Key string `mapstructure:"key"`
Enabled bool `mapstructure:"enabled"`
Segment Segment `mapstructure:"segment"`
}
type Segment struct {
Key string `mapstructure:"key"`
}
func NewConfigFactory() factory.ConfigFactory {
@@ -23,14 +25,20 @@ func NewConfigFactory() factory.ConfigFactory {
func newConfig() factory.Config {
return Config{
Enabled: false,
Key: key,
Segment: Segment{
Key: key,
},
}
}
func (c Config) Validate() error {
if c.Key != key {
return fmt.Errorf("cannot override key set at build time with key: %s", c.Key)
}
return nil
}
func (c Config) Provider() string {
if c.Enabled {
return "segment"
}
return "noop"
}

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