Compare commits

...

43 Commits

Author SHA1 Message Date
SagarRajput-7
3a5d3225dd feat(base-path): migrate backend bound urls and eslint upgrade to error 2026-04-20 15:42:10 +05:30
SagarRajput-7
b61376b904 feat(base-path): migrate remaining pattern for window.location.origin + path 2026-04-20 14:18:59 +05:30
SagarRajput-7
18429b3b91 feat(base-path): replace window.open with openInNewTab for internal paths 2026-04-20 14:05:16 +05:30
SagarRajput-7
c7adb22c61 feat: code refactor around feedbacks 2026-04-20 13:34:46 +05:30
SagarRajput-7
b936e63658 feat: applied suggested patch changes 2026-04-20 12:14:15 +05:30
SagarRajput-7
727b105b6c Merge branch 'main' into base-path-config-1 2026-04-20 12:02:01 +05:30
Nityananda Gohain
96d816bd1a feat: improve perf for queries with empty resource filter (#10861)
* feat: improve perf for queries with empty resource filter

* fix: update comment

* fix: add nolint:nilnil

* fix: address comments

* fix: update comment

---------

Co-authored-by: Ankit Nayan <ankit@signoz.io>
2026-04-20 03:30:16 +00:00
Pandey
f52d89c338 docs(go): add types.md covering core-type and Postable/Gettable/Storable conventions (#10998)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* docs(go): add types.md covering core-type and Postable/Gettable/Storable conventions

Document the core-type-first pattern used across pkg/types/: always define
the domain type X, and introduce Postable/Gettable/Updatable/Storable
flavors only when their shape actually differs from X. Walk through
Channel (core only), AuthDomain (all four flavors), and Rule (the
in-progress v2 split) as worked examples.

* docs(go): drop Rule migration example from types.md

* docs(go): allow both NewFromX and ToX conversion forms in types.md

* docs(go): move validation guidance to core type X in types.md

* docs(go): drop exact file:line refs in types.md, use inline examples

* docs(go): split spelling guidance into Updatable and Storable bullets

* docs(go): trim conversion examples to Channel and AuthDomain

* docs(go): swap remaining examples to AuthDomain/Channel where possible
2026-04-19 16:35:47 +00:00
aniketio-ctrl
691e919a41 feat(billing): add zeus put meters api (#10923)
* feat(billing): add zeus put meters api

* feat(billing): add zeus put meters api
2026-04-19 11:30:09 +00:00
Pandey
61ae49d4ab refactor(ruler): introduce v2 Rule read type and validate uuid on delete (#10997)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* refactor(ruler): add Rule v2 read type and rename storage Rule to StorableRule

* refactor(ruler): map GettableRule to Rule before responding on v2 routes

* docs(openapi): regenerate spec with RuletypesRule on v2 rules routes

* docs(frontend): regenerate API clients with RuletypesRuleDTO

* refactor(ruler): validate uuid-v7 on delete rule handler
2026-04-18 12:39:50 +00:00
Pandey
a5e1f71cf6 refactor(ruler): tighten rule and downtime OpenAPI schema (#10995)
* refactor(ruler): add Enum() on AlertType

* refactor(ruler): convert RepeatType and RepeatOn to valuer.String with Enum()

* refactor(ruler): mark required fields on Recurrence

* refactor(ruler): mark required tag on CumulativeSchedule.Type

* refactor(ruler): rename GettablePlannedMaintenance to PlannedMaintenance

* docs: regenerate OpenAPI spec and frontend clients with tightened schema

* refactor(ruler): add PostablePlannedMaintenance input type with Validate

* refactor(ruler): rename EditPlannedMaintenance to Update and GetAll to List

* refactor(ruler): switch Create/Update to *PostablePlannedMaintenance

* refactor(ruler): convert PlannedMaintenance.Id string to ID valuer.UUID

* refactor(ruler): return *PlannedMaintenance from CreatePlannedMaintenance

* docs: regenerate OpenAPI spec and frontend clients for Postable/ID changes

* refactor(ruler): type PlannedMaintenance.Status as MaintenanceStatus enum

* refactor(ruler): type PlannedMaintenance.Kind as MaintenanceKind enum

* refactor(ruler): mark GettableRule.Id required

* refactor(ruler): mark GettableRule.State required

* refactor(ruler): make GettableRule timestamps non-pointer and users nullable

* refactor(ruler): return bare array from v2 ListRules instead of wrapped object

* docs: regenerate OpenAPI spec and frontend clients for schema pass
2026-04-18 09:46:07 +00:00
Pandey
c9610df66d refactor(ruler): move rules and planned maintenance handlers to signozapiserver (#10957)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* refactor(ruler): define Ruler and Handler interfaces with signozruler implementation

Expand the Ruler interface with rule management and planned maintenance
methods matching rules.Manager signatures. Add Handler interface for
HTTP endpoints. Implement handler in signozruler wrapping ruler.Ruler,
and update provider to embed *rules.Manager for interface satisfaction.

* refactor(ruler): move eval_delay from query-service constants to ruler config

Replace constants.GetEvalDelay() with config.EvalDelay on ruler.Config,
defaulting to 2m. This removes the signozruler dependency on
pkg/query-service/constants.

* refactor(ruler): use time.Duration for eval_delay config

Match the convention used by all other configs in the codebase.
TextDuration is for preserving human-readable text through JSON
round-trips in user-facing rule definitions, not for internal config.

* refactor(ruler): add godoc comments and spacing to Ruler interface

* refactor(ruler): wire ruler handler through signoz.New and signozapiserver

- Add Start/Stop to Ruler interface for lifecycle management
- Add rulerCallback to signoz.New() for EE customization
- Wire ruler.Handler through Handlers, signozapiserver provider
- Register 12 routes in signozapiserver/ruler.go (7 rules, 5 downtime)
- Update cmd/community and cmd/enterprise to pass rulerCallback
- Move rules.Manager creation from server.go to signoz.New via callback
- Change APIHandler.ruleManager type from *rules.Manager to ruler.Ruler
- Remove makeRulesManager from both OSS and EE server.go

* refactor(ruler): remove old rules and downtime_schedules routes from http_handler

Remove 7 rules CRUD routes and 5 downtime_schedules routes plus their
handler methods from http_handler.go. These are now served by
signozapiserver/ruler.go via handler.New() with OpenAPIDef.

The 4 v1 history routes (stats, timeline, top_contributors,
overall_status) remain in http_handler.go as they depend on
interfaces.Reader and have v2 equivalents already in signozapiserver.

* refactor(ruler): use ProviderFactory pattern and register in factory.Registry

Replace the rulerCallback with rulerProviderFactories following the
standard ProviderFactory pattern (like auditorProviderFactories). The
ruler is now created via factory.NewProviderFromNamedMap and registered
in factory.Registry for lifecycle management. Start/Stop are no longer
called manually in server.go.

- Ruler interface embeds factory.Service (Start/Stop return error)
- signozruler.NewFactory accepts all deps including EE task funcs
- provider uses named field (not embedding) with explicit delegation
- cmd/community passes nil task funcs, cmd/enterprise passes EE funcs
- Remove NewRulerProviderFactories (replaced by callback from cmd/)
- Remove manual Start/Stop from both OSS and EE server.go

* fix(ruler): make Start block on stopC per factory.Service contract

rules.Manager.Start is non-blocking (run() just closes a channel).
Add stopC to provider so Start blocks until Stop closes it, matching
the factory.Service contract used by the Registry.

* refactor(ruler): remove unused RM() accessor from EE APIHandler

* refactor(ruler): remove RuleManager from APIHandlerOpts

Use Signoz.Ruler directly instead of passing it through opts.

* refactor(ruler): add /api/v1/rules/test and mark /api/v1/testRule as deprecated

* refactor(ruler): use binding.JSON.BindBody for downtime schedule decode

* refactor(ruler): add TODOs for raw string params on Ruler interface

Mark CreateRule, EditRule, PatchRule, TestNotification, and DeleteRule
with TODOs to accept typed params instead of raw JSON strings. Requires
changing the storage model since the manager stores raw JSON as Data.

* refactor(ruler): add TODO on MaintenanceStore to not expose store directly

* docs: regenerate OpenAPI spec and frontend API clients with ruler routes

* refactor(ruler): rename downtime_schedules tag to downtimeschedules

* refactor(ruler): add query params to ListDowntimeSchedules OpenAPIDef

Add ListPlannedMaintenanceParams struct with active/recurring fields.
Use binding.Query.BindQuery in the handler instead of raw URL parsing.
Add RequestQuery to the OpenAPIDef so params appear in the OpenAPI spec
and generated frontend client.

* refactor(ruler): add GettableTestRule response type to TestRule endpoint

Define GettableTestRule struct with AlertCount and Message fields.
Use it as the Response in TestRule OpenAPIDef so the generated frontend
client has a proper response type instead of string.

* refactor(ruler): tighten schema with oneOf unions and required fields

Surface the polymorphism in RuleThresholdData and EvaluationEnvelope via
JSONSchemaOneOf (the same pattern as QueryEnvelope), so the generated
TS types are discriminated unions with typed `spec` instead of unknown.
Also mark `alert`, `ruleType`, and `condition` required on PostableRule
so the generated TS types are non-optional for callers.

* refactor(ruler): add Enum() on EvaluationKind, ScheduleType, ThresholdKind

Surface the fixed set of accepted values for these valuer-wrapped kind
types so OpenAPI emits proper string-enum schemas and the generated TS
types become string-literal unions instead of plain string.

* refactor(ruler): mark required fields on nested rule and maintenance types

Surface fields already enforced by Validate()/UnmarshalJSON as required
in the OpenAPI schema so the generated TS types match runtime behavior.

Touches RuleCondition (compositeQuery, op, matchType), RuleThresholdData
(kind, spec), BasicRuleThreshold (name, target, op, matchType),
RollingWindow (evalWindow, frequency), CumulativeWindow (schedule,
frequency, timezone), EvaluationEnvelope (kind, spec), Schedule
(timezone), GettablePlannedMaintenance (name, schedule).

Does not mark server-populated fields (id, createdAt, updatedAt, status,
kind) on GettablePlannedMaintenance required, since the same struct is
reused for request bodies in MaintenanceStore.CreatePlannedMaintenance.

* refactor(ruler): tighten AlertCompositeQuery, QueryType, PanelType schema

Missed in the earlier tightening pass. AlertCompositeQuery.queries,
panelType, queryType are all required for a valid composite query;
QueryType and PanelType are valuer-wrapped with fixed value sets, so
expose them as enums in the OpenAPI schema.

* refactor(ruler): wrap sql.ErrNoRows as TypeNotFound in by-ID lookups

GetStoredRule and GetPlannedMaintenanceByID previously returned bun's
raw Scan error, so a missing ID leaked "sql: no rows in result set" to
the HTTP response with a 500 status. WrapNotFoundErrf converts
sql.ErrNoRows into TypeNotFound so render.Error emits 404 with a stable
`not_found` code, and passes other errors through unchanged.

* refactor(ruler): move migrated rules routes to /api/v2/rules

The 7 rules routes now live at /api/v2/rules, /api/v2/rules/{id}, and
/api/v2/rules/test — served via handler.New with render.Success and
render.Error. The legacy /api/v1/rules paths will be restored in the
query-service http handler in a follow-up so existing clients keep
receiving the SuccessResponse envelope unchanged.

Drop the /api/v1/testRule deprecated alias from signozapiserver; the
original lives on main's http_handler.go and is restored alongside the
other v1 paths.

Downtime schedule routes stay at /api/v1/downtime_schedules — single
track, no legacy restore planned.

* refactor(ruler): restore /api/v1/rules legacy handlers for back-compat

Bring the 7 rule CRUD/test handlers and their router.HandleFunc lines
back to http_handler.go so /api/v1/rules, /api/v1/rules/{id}, and
/api/v1/testRule continue to emit the legacy SuccessResponse envelope.
The v2 versions under signozapiserver are the new home for the render
envelope used by generated clients.

Delegation uses aH.ruleManager (populated from opts.Signoz.Ruler in
NewAPIHandler), so a single ruler.Ruler instance serves both paths — no
second rules.Manager is instantiated.

Downtime schedules stay single-track under signozapiserver; the 5
downtime handlers are not restored.

* docs: regenerate OpenAPI spec and frontend clients for /api/v2/rules

* refactor(ruler): return 201 Created on POST /api/v2/rules

A successful create now responds with 201 Created and the full
GettableRule body, matching REST convention for resource creation.
Regenerates the OpenAPI spec and frontend clients to reflect the new
status code.

* refactor(ruler): restore dropped sorter TODO in legacy listRules

The legacy listRules handler was copied verbatim from main during the
v1 back-compat restore, but an inner blank line and the load-bearing
`// todo(amol): need to add sorter` comment were stripped. Put them
back so the legacy block round-trips cleanly against main.

* refactor(ruler): return 201 Created on POST /api/v1/downtime_schedules

Match the REST convention already applied to POST /api/v2/rules:
successful creates respond with 201 Created. Response body remains
empty (nil); the generated frontend client surface is unchanged since
no response type was declared.

A richer "return the created resource" response body is a separate
follow-up — holding off until the ruletypes naming cleanup lands.

* fix(ruler): signal Healthy only after manager.Start closes m.block

The ruler provider didn't implement factory.Healthy, so the registry
fell back to factory.closedC and marked the service StateRunning the
instant its Start goroutine spawned — before rules.Manager.Start had
closed m.block. /api/v2/healthz therefore returned 200 while rule
evaluation was still gated, and integration tests that POSTed a rule
immediately after the readiness check saw their task goroutines stuck
on <-m.block until the next frequency tick.

Add a healthyC channel and close it inside Start only after
manager.Start returns; implement factory.Healthy so the registry and
/api/v2/healthz wait on the real readiness signal.

* fix: add the withhealthy interface

* fix(ruler): alias legacy RULES_EVAL_DELAY env var in backward-compat

The eval_delay config was moved from query-service constants (read from
RULES_EVAL_DELAY) onto ruler.Config (read via mapstructure from
SIGNOZ_RULER_EVAL__DELAY). That silently broke the legacy env var for
any existing deployment — notably the alerts integration-test fixture
which sets RULES_EVAL_DELAY=0s to let rules evaluate against just-
inserted data. The resulting default 2m delay pushed the query window
far enough back that the fixture's rate spike fell outside it, causing
8 of 24 parametrize cases in 02_basic_alert_conditions.py to fail with
"Expected N alerts to be fired but got 0 alerts".

Add RULES_EVAL_DELAY to mergeAndEnsureBackwardCompatibility alongside
the ~10 other aliased legacy env vars. Emits the standard deprecation
warning and overrides config.Ruler.EvalDelay.
2026-04-18 08:25:16 +00:00
Pandey
ef298af388 feat(apiserver): derive HTTP route prefix from global.external_url (#10943)
* feat(apiserver): derive HTTP route prefix from global.external_url

The path component of global.external_url is now used as the base path
for all HTTP routes (API and web frontend), enabling SigNoz to be served
behind a reverse proxy at a sub-path (e.g. https://example.com/signoz/).

The prefix is applied via http.StripPrefix at the outermost handler
level, requiring zero changes to route registration code. Health
endpoints (/api/v1/health, /api/v2/healthz, /api/v2/readyz,
/api/v2/livez) remain accessible without the prefix for container
healthchecks.

Removes web.prefix config in favor of the unified global.external_url
approach, avoiding the desync bugs seen in projects with separate
API/UI prefix configs (ArgoCD, Prometheus).

closes SigNoz/platform-pod#1775

* feat(web): template index.html with dynamic base href from global.external_url

Read index.html at startup, parse as Go template with [[ ]] delimiters,
execute with BasePath derived from global.external_url, and cache the
rendered bytes in memory. This injects <base href="/signoz/" /> (or
whatever the route prefix is) so the browser resolves relative URLs
correctly when SigNoz is served at a sub-path.

Inject global.Config into the routerweb provider via the factory closure
pattern. Static files (JS, CSS, images) are still served from disk
unchanged.

* refactor(web): extract index.html templating into web.NewIndex

Move the template parsing and execution logic from routerweb provider
into pkg/web/template.go. NewIndex logs and returns raw bytes on
template failure; NewIndexE returns the error for callers that need it.

Rename BasePath to BaseHref to match the HTML attribute it populates.
Inject global.Config into routerweb via the factory closure pattern.

* refactor(global): rename RoutePrefix to ExternalPath, add ExternalPathTrailing

Rename RoutePrefix() to ExternalPath() to accurately reflect what it
returns: the path component of the external URL. Add
ExternalPathTrailing() which returns the path with a trailing slash,
used for HTML base href injection.

* refactor(web): make index filename configurable via web.index

Move the hardcoded indexFileName const from routerweb/provider.go to
web.Config.Index with default "index.html". This allows overriding the
SPA entrypoint file via configuration.

* refactor(web): collapse testdata_basepath into testdata

Use a single testdata directory with a templated index.html for all
routerweb tests. Remove the redundant testdata_basepath directory.

* test(web): add no-template and invalid-template index test cases

Add three distinct index fixtures in testdata:
- index.html: correct [[ ]] template with BaseHref
- index_no_template.html: plain HTML, no placeholders
- index_invalid_template.html: malformed template syntax

Tests verify: template substitution works, plain files pass through
unchanged, and invalid templates fall back to serving raw bytes.
Consolidate test helpers into startServer/get.

* refactor(web): rename test fixtures to no_template, valid_template, invalid_template

Drop the index_ prefix from test fixtures. Use web instead of w for
the variable name in test helpers.

* test(web): add SPA fallback paths to no_template and invalid_template tests

Test /, /does-not-exist, and /assets in all three template test cases
to verify SPA fallback behavior (non-existent paths and directories
serve the index) regardless of template type.

* test(web): use exact match instead of contains in template tests

Match the full expected response body in TestServeTemplatedIndex
instead of using assert.Contains.

* style(web): use raw string literals for expected test values

* refactor(web): rename get test helper to httpGet

* refactor(web): use table-driven tests with named path cases

Replace for-loop path iteration with explicit table-driven test cases
for each path. Each path (root, non-existent, directory) is a named
subtest case in all three template tests.

* chore: remove redundant comments from added code

* style: add blank lines between logical blocks

* fix(web): resolve lint errors in provider and template

Fix errcheck on rw.Write in serveIndex, use ErrorContext instead of
Error in NewIndex for sloglint compliance. Move serveIndex below
ServeHTTP to order public methods before private ones.

* style: formatting and test cleanup from review

Restructure Validate nil check, rename expectErr to fail with
early-return, trim trailing newlines in test assertions, remove
t.Parallel from subtests, inline short config literals, restore
struct field comments in web.Config.

* fix: remove unused files

* fix: remove unused files

* perf(web): cache http.FileServer on provider instead of creating per-request

* refactor(web): use html/template for context-aware escaping in index rendering

---------

Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com>
2026-04-18 06:47:17 +00:00
Yunus M
b5c146afdf chore: update @signozhq packages and adjust styles in AuthHeader and AnnouncementTooltip components (#10989)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-04-17 14:41:22 +00:00
Abhishek Kumar Singh
dfbcd1e0ec feat: AlertManager templater (#10581)
* chore: custom notifiers in alert manager

* chore: lint fixs

* chore: fix email linter

* chore: added tracing to msteamsv2 notifier

* feat: alert manager template to template title and notification body

* chore: updated test name + code for timeout errors

* chore: added utils for using variables with $ notation

* chore: exposed templates for alertmanager types

* feat: added preprocessor for alert templater

* chore: hooked preProcess function in expandTitle and body, added labels and annotations in alertdata

* chore: fix lint issues

* chore: added handling for missing variable used in template

* feat: converted alerttemplater to interface and updated tests

* refactor: added extractCommonKV instead of 2 different functions

* test: fix preprocessor test case

* feat: added support for  and  in templating

* chore: lint fix

* chore: renamed the interface

* chore: added test for missing function

* refactor: test case and sb related changed

* refactor: comments and test improvements

* chore: lint fix

* chore: updated comments

* chore: updated newline to markdown format

* chore: updated br with new line in test and logs added

* refactor: review comments

* refactor: lint fixes

* chore: updated licenses for notifiers

* chore: updated email notifier from upstream

* feat: return single templating result from  with flag for template type

* fix: variables with symbols in template

* chore: removed notifier test files

* refactor: changes as per internal review

* chore: lint issue

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
2026-04-17 13:00:15 +00:00
SagarRajput-7
f3269318b7 feat: code refactor around feedbacks 2026-04-17 15:58:00 +05:30
SagarRajput-7
5088bd7499 feat: updated base path utils and fixed navigation and translations 2026-04-17 13:52:12 +05:30
SagarRajput-7
7648d4f3d3 feat: updated the html template 2026-04-16 21:55:21 +05:30
SagarRajput-7
5729a4584a feat: removed plugin and serving the index.html only as the template 2026-04-16 18:23:46 +05:30
SagarRajput-7
825d06249d feat: refactor the interceptor and added gotmpl into gitignore 2026-04-16 18:23:38 +05:30
SagarRajput-7
9034471587 feat: changed output path to dir level 2026-04-16 18:23:25 +05:30
SagarRajput-7
4cc23ead6b feat: base path config setup and plugin for gotmpl generation at build time 2026-04-16 18:19:07 +05:30
SagarRajput-7
867e27d45f Merge branch 'main' into platform-pod/issues/1775 2026-04-16 18:17:11 +05:30
grandwizard28
be37e588f8 perf(web): cache http.FileServer on provider instead of creating per-request 2026-04-16 14:53:06 +05:30
grandwizard28
057dcbe6e4 fix: remove unused files 2026-04-16 02:26:18 +05:30
grandwizard28
3a28d741a3 fix: remove unused files 2026-04-16 02:24:20 +05:30
grandwizard28
223e83154f style: formatting and test cleanup from review
Restructure Validate nil check, rename expectErr to fail with
early-return, trim trailing newlines in test assertions, remove
t.Parallel from subtests, inline short config literals, restore
struct field comments in web.Config.
2026-04-16 02:17:14 +05:30
grandwizard28
50ae51cdaa fix(web): resolve lint errors in provider and template
Fix errcheck on rw.Write in serveIndex, use ErrorContext instead of
Error in NewIndex for sloglint compliance. Move serveIndex below
ServeHTTP to order public methods before private ones.
2026-04-16 02:05:25 +05:30
grandwizard28
c8ae8476c3 style: add blank lines between logical blocks 2026-04-16 01:57:24 +05:30
grandwizard28
daaa66e1fc chore: remove redundant comments from added code 2026-04-16 01:54:14 +05:30
grandwizard28
b0717d6a69 refactor(web): use table-driven tests with named path cases
Replace for-loop path iteration with explicit table-driven test cases
for each path. Each path (root, non-existent, directory) is a named
subtest case in all three template tests.
2026-04-16 01:49:07 +05:30
grandwizard28
4aefe44313 refactor(web): rename get test helper to httpGet 2026-04-16 01:47:35 +05:30
grandwizard28
4dc6f6fe7b style(web): use raw string literals for expected test values 2026-04-16 01:44:46 +05:30
grandwizard28
d3e0c46ba2 test(web): use exact match instead of contains in template tests
Match the full expected response body in TestServeTemplatedIndex
instead of using assert.Contains.
2026-04-16 01:43:23 +05:30
grandwizard28
0fed17e11a test(web): add SPA fallback paths to no_template and invalid_template tests
Test /, /does-not-exist, and /assets in all three template test cases
to verify SPA fallback behavior (non-existent paths and directories
serve the index) regardless of template type.
2026-04-16 01:38:46 +05:30
grandwizard28
a2264b4960 refactor(web): rename test fixtures to no_template, valid_template, invalid_template
Drop the index_ prefix from test fixtures. Use web instead of w for
the variable name in test helpers.
2026-04-16 01:32:50 +05:30
grandwizard28
2740964106 test(web): add no-template and invalid-template index test cases
Add three distinct index fixtures in testdata:
- index.html: correct [[ ]] template with BaseHref
- index_no_template.html: plain HTML, no placeholders
- index_invalid_template.html: malformed template syntax

Tests verify: template substitution works, plain files pass through
unchanged, and invalid templates fall back to serving raw bytes.
Consolidate test helpers into startServer/get.
2026-04-16 01:28:37 +05:30
grandwizard28
0ca22dd7fe refactor(web): collapse testdata_basepath into testdata
Use a single testdata directory with a templated index.html for all
routerweb tests. Remove the redundant testdata_basepath directory.
2026-04-16 01:22:54 +05:30
grandwizard28
a3b6bddac8 refactor(web): make index filename configurable via web.index
Move the hardcoded indexFileName const from routerweb/provider.go to
web.Config.Index with default "index.html". This allows overriding the
SPA entrypoint file via configuration.
2026-04-16 01:19:35 +05:30
grandwizard28
d908ce321a refactor(global): rename RoutePrefix to ExternalPath, add ExternalPathTrailing
Rename RoutePrefix() to ExternalPath() to accurately reflect what it
returns: the path component of the external URL. Add
ExternalPathTrailing() which returns the path with a trailing slash,
used for HTML base href injection.
2026-04-16 01:13:16 +05:30
grandwizard28
c221a44f3d refactor(web): extract index.html templating into web.NewIndex
Move the template parsing and execution logic from routerweb provider
into pkg/web/template.go. NewIndex logs and returns raw bytes on
template failure; NewIndexE returns the error for callers that need it.

Rename BasePath to BaseHref to match the HTML attribute it populates.
Inject global.Config into routerweb via the factory closure pattern.
2026-04-16 01:08:46 +05:30
grandwizard28
22fb4daaf9 feat(web): template index.html with dynamic base href from global.external_url
Read index.html at startup, parse as Go template with [[ ]] delimiters,
execute with BasePath derived from global.external_url, and cache the
rendered bytes in memory. This injects <base href="/signoz/" /> (or
whatever the route prefix is) so the browser resolves relative URLs
correctly when SigNoz is served at a sub-path.

Inject global.Config into the routerweb provider via the factory closure
pattern. Static files (JS, CSS, images) are still served from disk
unchanged.
2026-04-16 00:58:20 +05:30
grandwizard28
1bdc059d76 feat(apiserver): derive HTTP route prefix from global.external_url
The path component of global.external_url is now used as the base path
for all HTTP routes (API and web frontend), enabling SigNoz to be served
behind a reverse proxy at a sub-path (e.g. https://example.com/signoz/).

The prefix is applied via http.StripPrefix at the outermost handler
level, requiring zero changes to route registration code. Health
endpoints (/api/v1/health, /api/v2/healthz, /api/v2/readyz,
/api/v2/livez) remain accessible without the prefix for container
healthchecks.

Removes web.prefix config in favor of the unified global.external_url
approach, avoiding the desync bugs seen in projects with separate
API/UI prefix configs (ArgoCD, Prometheus).

closes SigNoz/platform-pod#1775
2026-04-16 00:38:55 +05:30
170 changed files with 8100 additions and 1482 deletions

View File

@@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra"
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
@@ -14,6 +15,7 @@ import (
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
"github.com/SigNoz/signoz/pkg/authz/openfgaschema"
"github.com/SigNoz/signoz/pkg/authz/openfgaserver"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway"
@@ -26,14 +28,20 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
"github.com/SigNoz/signoz/pkg/zeus/noopzeus"
@@ -75,7 +83,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
return signoz.NewSQLSchemaProviderFactories(sqlstore)
},
@@ -107,6 +115,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(_ sqlstore.SQLStore, _ global.Global, _ zeus.Zeus, _ gateway.Gateway, _ licensing.Licensing, _ serviceaccount.Module, _ cloudintegration.Config) (cloudintegration.Module, error) {
return implcloudintegration.NewModule(), nil
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, nil, nil))
},
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))

View File

@@ -22,14 +22,17 @@ import (
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
eequerier "github.com/SigNoz/signoz/ee/querier"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
eerules "github.com/SigNoz/signoz/ee/query-service/rules"
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/gateway"
@@ -40,15 +43,21 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/ruler"
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/sqlstore/sqlstorehook"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/SigNoz/signoz/pkg/version"
"github.com/SigNoz/signoz/pkg/zeus"
)
@@ -96,7 +105,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),
signoz.NewWebProviderFactories(config.Global),
func(sqlstore sqlstore.SQLStore) factory.NamedMap[factory.ProviderFactory[sqlschema.SQLSchema, sqlschema.Config]] {
existingFactories := signoz.NewSQLSchemaProviderFactories(sqlstore)
if err := existingFactories.Add(postgressqlschema.NewFactory(sqlstore)); err != nil {
@@ -166,6 +175,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return implcloudintegration.NewModule(pkgcloudintegration.NewStore(sqlStore), global, zeus, gateway, licensing, serviceAccount, cloudProvidersMap, config)
},
func(c cache.Cache, am alertmanager.Alertmanager, ss sqlstore.SQLStore, ts telemetrystore.TelemetryStore, ms telemetrytypes.MetadataStore, p prometheus.Prometheus, og organization.Getter, rsh rulestatehistory.Module, q querier.Querier, qp queryparser.QueryParser) factory.NamedMap[factory.ProviderFactory[ruler.Ruler, ruler.Config]] {
return factory.MustNewNamedMap(signozruler.NewFactory(c, am, ss, ts, ms, p, og, rsh, q, qp, eerules.PrepareTaskFunc, eerules.TestNotification))
},
)
if err != nil {
logger.ErrorContext(ctx, "failed to create signoz", errors.Attr(err))

View File

@@ -6,6 +6,8 @@
##################### Global #####################
global:
# the url under which the signoz apiserver is externally reachable.
# the path component (e.g. /signoz in https://example.com/signoz) is used
# as the base path for all HTTP routes (both API and web frontend).
external_url: <unset>
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
ingestion_url: <unset>
@@ -50,8 +52,8 @@ pprof:
web:
# Whether to enable the web frontend
enabled: true
# The prefix to serve web on
prefix: /
# The index file to use as the SPA entrypoint.
index: index.html
# The directory containing the static build files.
directory: /etc/signoz/web

File diff suppressed because it is too large Load Diff

View File

@@ -20,3 +20,4 @@ We **recommend** (almost enforce) reviewing these guides before contributing to
- [Packages](packages.md) - Naming, layout, and conventions for `pkg/` packages
- [Service](service.md) - Managed service lifecycle with `factory.Service`
- [SQL](sql.md) - Database and SQL patterns
- [Types](types.md) - Domain types, request/response bodies, and storage rows in `pkg/types/`

View File

@@ -0,0 +1,152 @@
# Types
Domain types in `pkg/types/<domain>/` live on three serialization boundaries — inbound HTTP, outbound HTTP, and SQL — on top of an in-memory domain representation. SigNoz's convention is **core-type-first**: every domain defines a single canonical type `X`, and specialized flavors (`PostableX`, `GettableX`, `UpdatableX`, `StorableX`) are introduced **only when they actually differ from `X`**. This guide spells out when each flavor is warranted and how they relate to each other.
Before reading, make sure you have read [abstractions.md](abstractions.md) — the rules here build on its guidance that every new type must earn its place.
## The core type is required
Every domain package in `pkg/types/<domain>/` defines exactly one core type `X`: `AuthDomain`, `Channel`, `Rule`, `Dashboard`, `Role`, `PlannedMaintenance`. This is the canonical in-memory representation of the domain object. Domain methods, validation invariants, and business logic hang off `X` — not off the flavor types.
Two rules shape how the core type behaves:
- **Conversions can be either `New<Output>From<Input>` or a receiver-style `(x *X) ToY()` method.** Either form is fine; pick whichever reads best at the call site:
```go
// Constructor form
func NewGettableAuthDomainFromAuthDomain(d *AuthDomain, info *AuthNProviderInfo) *GettableAuthDomain
// Receiver form
func (m *PlannedMaintenanceWithRules) ToPlannedMaintenance() *PlannedMaintenance
```
- **`X` can double as the storage row** when the DB shape would be identical. `Channel` embeds `bun.BaseModel` directly, and there is no `StorableChannel`. This is the preferred shape when it works.
Domain packages under `pkg/types/` must not import from other `pkg/` packages. Keep the core type's methods lightweight and push orchestration out to the module layer.
## Add a flavor only when it differs
For each of the four flavors, create it only if its shape diverges from `X`. If a flavor would have the same fields and tags as `X`, reuse `X` directly, or declare a type alias. Every flavor must earn its place per [abstractions.md](abstractions.md) rule 6 ("Wrappers must add semantics, not just rename").
| Flavor | Create it when it differs in… |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PostableX` | JSON shape differs from `X` — typically no `Id`, no audit fields, no server-computed fields. Often owns input validation via `Validate()` or a custom `UnmarshalJSON`. |
| `GettableX` | Response shape adds server-computed fields that are not persisted — e.g., `GettableAuthDomain` adds `AuthNProviderInfo`, which is resolved at read time. |
| `UpdatableX` | Only a strict subset of `PostableX` is replaceable on PUT. If the updatable shape equals `PostableX`, reuse `PostableX`. |
| `StorableX` | DB row shape differs from `X` — usually `X` carries nested typed config while `StorableX` carries a flat `Data string` JSON column, plus bun tags, audit mixins, and an `OrgID`. If `X` already has those, skip the flavor. |
The failure mode this rule exists to prevent: minting all four flavors on reflex for every new resource, even when two or three are structurally identical. Each unnecessary flavor is another type contributors must understand and another conversion that can drift.
## Worked examples
### Channel — core type only
```go
type Channels = []*Channel
type GettableChannels = []*Channel
type Channel struct {
bun.BaseModel `bun:"table:notification_channel"`
types.Identifiable
types.TimeAuditable
Name string `json:"name" required:"true" bun:"name"`
Type string `json:"type" required:"true" bun:"type"`
Data string `json:"data" required:"true" bun:"data"`
OrgID string `json:"orgId" required:"true" bun:"org_id"`
}
```
`Channel` is both the domain type and the bun row. `GettableChannels` is a **type alias** because `*Channel` already serializes correctly as a response. There is no `StorableChannel`, `PostableChannel`, or `UpdatableChannel` — those would be identical to `Channel` and so do not exist. Prefer this shape when it works.
### AuthDomain — all four flavors
```go
type AuthDomain struct {
storableAuthDomain *StorableAuthDomain
authDomainConfig *AuthDomainConfig
}
type StorableAuthDomain struct {
bun.BaseModel `bun:"table:auth_domain"`
types.Identifiable
Name string `bun:"name"`
Data string `bun:"data"` // AuthDomainConfig serialized as JSON
OrgID valuer.UUID `bun:"org_id"`
types.TimeAuditable
}
type PostableAuthDomain struct {
Config AuthDomainConfig `json:"config"`
Name string `json:"name"`
}
type UpdateableAuthDomain struct {
Config AuthDomainConfig `json:"config"` // Name intentionally absent
}
type GettableAuthDomain struct {
*StorableAuthDomain
*AuthDomainConfig
AuthNProviderInfo *AuthNProviderInfo `json:"authNProviderInfo"`
}
```
Each flavor exists for a concrete reason:
- `StorableAuthDomain` stores the typed config as an opaque `Data string` column, so the schema does not need to migrate every time a config field is added.
- `PostableAuthDomain` carries the config as a structured object (not a string) for the request.
- `UpdateableAuthDomain` excludes `Name` because a domain's name cannot change after creation.
- `GettableAuthDomain` adds `AuthNProviderInfo`, which is derived at read time and never persisted.
The core `AuthDomain` holds the two live halves — `storableAuthDomain` and `authDomainConfig` — and owns business methods such as `Update(config)`. Conversions use the `New<Output>From<Input>` form: `NewAuthDomainFromConfig`, `NewAuthDomainFromStorableAuthDomain`, `NewGettableAuthDomainFromAuthDomain`.
## Conventions that tie the flavors together
- **Conversions** use either a `New<Output>From<Input>` constructor — e.g. `NewChannelFromReceiver`, `NewGettableAuthDomainFromAuthDomain` — or a receiver-style `ToY()` method. Both forms coexist in the codebase; use whichever fits the call site.
- **Validation belongs on the core type `X`.** Putting it on `X` means every write path — HTTP create, HTTP update, in-process migration, replay — runs the same checks. `Validate()` on `PostableX` is reserved for checks that are specific to the request shape and do not apply to `X`. `UnmarshalJSON` on `PostableX` is a separate tool that lives there because decoding only happens at the HTTP boundary — `PostableAuthDomain.UnmarshalJSON` rejecting a malformed domain name at decode time is the canonical example.
```go
// Domain invariants: every write path re-runs these.
func (x *X) Validate() error { ... }
// Request-shape-only: checks that do not apply once the value is persisted.
func (p *PostableX) Validate() error { ... }
```
- **Type aliases, not wrappers**, when two shapes are identical. `type GettableChannels = []*Channel` is correct because it adds no semantics beyond the underlying type.
- **Serialization tags** follow [handler.md](handler.md): `required:"true"` means the JSON key must be present, `nullable:"true"` is required on any slice or map that may serialize as `null`, and types with a fixed value set must implement `Enum() []any`.
## A note on `UpdatableX` and `PatchableX`
- `UpdatableX` — the body for PUT (full replace) when the shape is a strict subset of `PostableX`. If the updatable shape equals `PostableX`, reuse `PostableX`.
- `PatchableX` — the body for PATCH (partial update); only the fields a client is allowed to patch. For example, `PatchableRole` carries a single `Description` field even though `Role` has many — clients may patch the description but not anything else.
```go
type PatchableRole struct {
Description string `json:"description"`
}
```
Both are optional. Do not introduce them if `PostableX` already covers the case.
## What to avoid
- **Do not mint a flavor that mirrors the core type.** If `StorableX` would have the same fields as `X`, use `X` directly with `bun.BaseModel` embedded. `Channel` is the canonical example.
- **Do not bolt domain methods onto `StorableX`.** Storage types are data carriers. Domain methods live on `X`.
- **Do not invent new suffixes** (`Creatable`, `Fetchable`, `Savable`). The core type plus `Postable` / `Gettable` / `Updatable` / `Patchable` / `Storable` covers every case that exists today.
- **Spelling — `Updatable`, not `Updateable`.** `Updateable` is a common typo. Prefer the shorter form when introducing new types, and rename any stragglers you come across.
- **Spelling — `Storable`, not `Storeable`.** `Storeable` is a common typo. Prefer the shorter form when introducing new types, and rename any stragglers you come across.
## What should I remember?
- Every domain package defines the core type `X`. Only `X` is mandatory.
- Add `PostableX` / `GettableX` / `UpdatableX` / `StorableX` one at a time, only when the shape actually diverges from `X`.
- Domain logic lives on `X`, not on the flavor types.
- Conversions can be a `New<Output>From<Input>` constructor or a receiver-style `ToY()` method — pick whichever reads best at the call site.
- Use a type alias when two shapes are truly identical.
- `pkg/types/<domain>/` must not import from other `pkg/` packages.
## Further reading
- [abstractions.md](abstractions.md) — when to introduce a new type at all.
- [handler.md](handler.md) — struct tag rules at the HTTP boundary.
- [packages.md](packages.md) — where types live under `pkg/types/`.
- [sql.md](sql.md) — star-schema requirements for `StorableX`.

View File

@@ -14,7 +14,6 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/app/logparsingpipeline"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
rules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/version"
@@ -23,7 +22,6 @@ import (
type APIHandlerOptions struct {
DataConnector interfaces.Reader
RulesManager *rules.Manager
UsageManager *usage.Manager
IntegrationsController *integrations.Controller
CloudIntegrationsController *cloudintegrations.Controller
@@ -43,7 +41,6 @@ type APIHandler struct {
func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.Config) (*APIHandler, error) {
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
Reader: opts.DataConnector,
RuleManager: opts.RulesManager,
IntegrationsController: opts.IntegrationsController,
CloudIntegrationsController: opts.CloudIntegrationsController,
LogsParsingPipelineController: opts.LogsParsingPipelineController,
@@ -64,10 +61,6 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz, config signoz.
return ah, nil
}
func (ah *APIHandler) RM() *rules.Manager {
return ah.opts.RulesManager
}
func (ah *APIHandler) UM() *usage.Manager {
return ah.opts.UsageManager
}

View File

@@ -12,10 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/cache/memorycache"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/ruler/rulestore/sqlrulestore"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/gorilla/handlers"
@@ -23,18 +19,10 @@ import (
"github.com/soheilhy/cmux"
"github.com/SigNoz/signoz/ee/query-service/app/api"
"github.com/SigNoz/signoz/ee/query-service/rules"
"github.com/SigNoz/signoz/ee/query-service/usage"
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/telemetrystore"
"github.com/SigNoz/signoz/pkg/web"
"log/slog"
@@ -49,7 +37,6 @@ import (
opAmpModel "github.com/SigNoz/signoz/pkg/query-service/app/opamp/model"
baseconst "github.com/SigNoz/signoz/pkg/query-service/constants"
"github.com/SigNoz/signoz/pkg/query-service/healthcheck"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/query-service/utils"
)
@@ -57,7 +44,6 @@ import (
type Server struct {
config signoz.Config
signoz *signoz.SigNoz
ruleManager *baserules.Manager
// public http router
httpConn net.Listener
@@ -97,24 +83,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
nil,
)
rm, err := makeRulesManager(
signoz.Cache,
signoz.Alertmanager,
signoz.SQLStore,
signoz.TelemetryStore,
signoz.TelemetryMetadataStore,
signoz.Prometheus,
signoz.Modules.OrgGetter,
signoz.Modules.RuleStateHistory,
signoz.Querier,
signoz.Instrumentation.ToProviderSettings(),
signoz.QueryParser,
)
if err != nil {
return nil, err
}
// initiate opamp
opAmpModel.Init(signoz.SQLStore, signoz.Instrumentation.Logger(), signoz.Modules.OrgGetter)
@@ -152,7 +120,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
}
// start the usagemanager
usageManager, err := usage.New(signoz.Licensing, signoz.TelemetryStore.ClickhouseDB(), signoz.Zeus, signoz.Modules.OrgGetter)
usageManager, err := usage.New(signoz.Licensing, signoz.TelemetryStore.ClickhouseDB(), signoz.Zeus, signoz.Modules.OrgGetter, signoz.Flagger)
if err != nil {
return nil, err
}
@@ -163,7 +131,6 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
apiOpts := api.APIHandlerOptions{
DataConnector: reader,
RulesManager: rm,
UsageManager: usageManager,
IntegrationsController: integrationsController,
CloudIntegrationsController: cloudIntegrationsController,
@@ -180,8 +147,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
s := &Server{
config: config,
signoz: signoz,
ruleManager: rm,
signoz: signoz,
httpHostPort: baseconst.HTTPHostPort,
unavailableChannel: make(chan healthcheck.Status),
usageManager: usageManager,
@@ -262,6 +228,20 @@ func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*h
return nil, err
}
routePrefix := s.config.Global.ExternalPath()
if routePrefix != "" {
prefixed := http.StripPrefix(routePrefix, handler)
handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/api/v1/health", "/api/v2/healthz", "/api/v2/readyz", "/api/v2/livez":
r.ServeHTTP(w, req)
return
}
prefixed.ServeHTTP(w, req)
})
}
return &http.Server{
Handler: handler,
}, nil
@@ -288,8 +268,6 @@ func (s *Server) initListeners() error {
// Start listening on http and private http port concurrently
func (s *Server) Start(ctx context.Context) error {
s.ruleManager.Start(ctx)
err := s.initListeners()
if err != nil {
return err
@@ -333,47 +311,9 @@ func (s *Server) Stop(ctx context.Context) error {
s.opampServer.Stop()
if s.ruleManager != nil {
s.ruleManager.Stop(ctx)
}
// stop usage manager
s.usageManager.Stop(ctx)
return nil
}
func makeRulesManager(cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
// create manager opts
managerOpts := &baserules.ManagerOptions{
TelemetryStore: telemetryStore,
MetadataStore: metadataStore,
Prometheus: prometheus,
Context: context.Background(),
Querier: querier,
Logger: providerSettings.Logger,
Cache: cache,
EvalDelay: baseconst.GetEvalDelay(),
PrepareTaskFunc: rules.PrepareTaskFunc,
PrepareTestRuleFunc: rules.TestNotification,
Alertmanager: alertmanager,
OrgGetter: orgGetter,
RuleStore: ruleStore,
MaintenanceStore: maintenanceStore,
SQLStore: sqlstore,
QueryParser: queryParser,
RuleStateHistoryModule: ruleStateHistoryModule,
}
// create Manager
manager, err := baserules.NewManager(managerOpts)
if err != nil {
return nil, fmt.Errorf("rule manager error: %v", err)
}
slog.Info("rules manager is ready")
return manager, nil
}

View File

@@ -16,9 +16,11 @@ import (
"github.com/SigNoz/signoz/ee/query-service/model"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/flagger"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/query-service/utils/encryption"
"github.com/SigNoz/signoz/pkg/types/featuretypes"
"github.com/SigNoz/signoz/pkg/zeus"
)
@@ -43,15 +45,18 @@ type Manager struct {
zeus zeus.Zeus
orgGetter organization.Getter
flagger flagger.Flagger
}
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, orgGetter organization.Getter) (*Manager, error) {
func New(licenseService licensing.Licensing, clickhouseConn clickhouse.Conn, zeus zeus.Zeus, orgGetter organization.Getter, flagger flagger.Flagger) (*Manager, error) {
m := &Manager{
clickhouseConn: clickhouseConn,
licenseService: licenseService,
scheduler: gocron.NewScheduler(time.UTC).Every(1).Day().At("00:00"), // send usage every at 00:00 UTC
zeus: zeus,
orgGetter: orgGetter,
flagger: flagger,
}
return m, nil
}
@@ -168,7 +173,14 @@ func (lm *Manager) UploadUsage(ctx context.Context) {
return
}
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
evalCtx := featuretypes.NewFlaggerEvaluationContext(organization.ID)
useZeus := lm.flagger.BooleanOrEmpty(ctx, flagger.FeaturePutMetersInZeus, evalCtx)
if useZeus {
errv2 = lm.zeus.PutMetersV2(ctx, payload.LicenseKey.String(), body)
} else {
errv2 = lm.zeus.PutMeters(ctx, payload.LicenseKey.String(), body)
}
if errv2 != nil {
slog.ErrorContext(ctx, "failed to upload usage", errors.Attr(errv2))
// not returning error here since it is captured in the failed count

View File

@@ -136,6 +136,18 @@ func (provider *Provider) PutMeters(ctx context.Context, key string, data []byte
return err
}
func (provider *Provider) PutMetersV2(ctx context.Context, key string, data []byte) error {
_, err := provider.do(
ctx,
provider.config.URL.JoinPath("/v1/meters"),
http.MethodPost,
key,
data,
)
return err
}
func (provider *Provider) PutProfile(ctx context.Context, key string, profile *zeustypes.PostableProfile) error {
body, err := json.Marshal(profile)
if err != nil {

View File

@@ -66,6 +66,8 @@ module.exports = {
rules: {
// Asset migration — base-path safety
'rulesdir/no-unsupported-asset-pattern': 'error',
// Base-path safety — window.open and origin-concat patterns
'rulesdir/no-raw-absolute-path': 'error',
// Code quality rules
'prefer-const': 'error', // Enforces const for variables never reassigned
@@ -246,6 +248,8 @@ module.exports = {
'sonarjs/cognitive-complexity': 'off', // Tests can be complex
'sonarjs/no-identical-functions': 'off', // Similar test patterns are OK
'sonarjs/no-small-switch': 'off', // Small switches are OK in tests
// Test assertions intentionally reference window.location.origin for expected-value checks
'rulesdir/no-raw-absolute-path': 'off',
},
},
{

View File

@@ -0,0 +1,153 @@
'use strict';
/**
* ESLint rule: no-raw-absolute-path
*
* Catches patterns that break at runtime when the app is served from a
* sub-path (e.g. /signoz/):
*
* 1. window.open(path, '_blank')
* → use openInNewTab(path) which calls withBasePath internally
*
* 2. window.location.origin + path / `${window.location.origin}${path}`
* → use getAbsoluteUrl(path)
*
* 3. frontendBaseUrl: window.location.origin (bare origin usage)
* → use getBaseUrl() to include the base path
*
* 4. window.location.href = path
* → use withBasePath(path) or navigate() for internal navigation
*
* External URLs (first arg starts with "http") are explicitly allowed.
*/
function isOriginAccess(node) {
return (
node.type === 'MemberExpression' &&
!node.computed &&
node.property.name === 'origin' &&
node.object.type === 'MemberExpression' &&
!node.object.computed &&
node.object.property.name === 'location' &&
node.object.object.type === 'Identifier' &&
node.object.object.name === 'window'
);
}
function isHrefAccess(node) {
return (
node.type === 'MemberExpression' &&
!node.computed &&
node.property.name === 'href' &&
node.object.type === 'MemberExpression' &&
!node.object.computed &&
node.object.property.name === 'location' &&
node.object.object.type === 'Identifier' &&
node.object.object.name === 'window'
);
}
function isExternalUrl(node) {
if (node.type === 'Literal' && typeof node.value === 'string') {
return node.value.startsWith('http://') || node.value.startsWith('https://');
}
if (node.type === 'TemplateLiteral' && node.quasis.length > 0) {
const raw = node.quasis[0].value.raw;
return raw.startsWith('http://') || raw.startsWith('https://');
}
return false;
}
// window.open(withBasePath(x)) and window.open(getAbsoluteUrl(x)) are already safe.
function isSafeHelperCall(node) {
return (
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
(node.callee.name === 'withBasePath' || node.callee.name === 'getAbsoluteUrl')
);
}
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow raw window.open and origin-concatenation patterns that miss the runtime base path',
category: 'Base Path Safety',
},
schema: [],
messages: {
windowOpen:
'Use openInNewTab(path) instead of window.open(path, "_blank") — openInNewTab prepends the base path automatically.',
originConcat:
'Use getAbsoluteUrl(path) instead of window.location.origin + path — getAbsoluteUrl prepends the base path automatically.',
originDirect:
'Use getBaseUrl() instead of window.location.origin — getBaseUrl includes the base path.',
hrefAssign:
'Use withBasePath(path) or navigate() instead of window.location.href = path — ensures the base path is included.',
},
},
create(context) {
return {
// window.open(path, ...) — allow only external first-arg URLs
CallExpression(node) {
const { callee, arguments: args } = node;
if (
callee.type !== 'MemberExpression' ||
callee.object.type !== 'Identifier' ||
callee.object.name !== 'window' ||
callee.property.name !== 'open'
)
return;
if (args.length < 1) return;
if (isExternalUrl(args[0])) return;
if (isSafeHelperCall(args[0])) return;
context.report({ node, messageId: 'windowOpen' });
},
// window.location.origin + path
BinaryExpression(node) {
if (node.operator !== '+') return;
if (isOriginAccess(node.left) || isOriginAccess(node.right)) {
context.report({ node, messageId: 'originConcat' });
}
},
// `${window.location.origin}${path}`
TemplateLiteral(node) {
if (node.expressions.some(isOriginAccess)) {
context.report({ node, messageId: 'originConcat' });
}
},
// window.location.origin used directly (not in concatenation)
// Catches: frontendBaseUrl: window.location.origin
MemberExpression(node) {
if (!isOriginAccess(node)) return;
const parent = node.parent;
// Skip if parent is BinaryExpression with + (handled by BinaryExpression visitor)
if (parent.type === 'BinaryExpression' && parent.operator === '+') return;
// Skip if inside TemplateLiteral (handled by TemplateLiteral visitor)
if (parent.type === 'TemplateLiteral') return;
context.report({ node, messageId: 'originDirect' });
},
// window.location.href = path
AssignmentExpression(node) {
if (node.operator !== '=') return;
if (!isHrefAccess(node.left)) return;
// Allow external URLs
if (isExternalUrl(node.right)) return;
// Allow safe helper calls
if (isSafeHelperCall(node.right)) return;
context.report({ node, messageId: 'hrefAssign' });
},
};
},
};

View File

@@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<base href="[[.BaseHref]]" />
<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate, max-age: 0"
@@ -39,12 +40,12 @@
<meta
data-react-helmet="true"
property="og:image"
content="/images/signoz-hero-image.webp"
content="[[.BaseHref]]images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
name="twitter:image"
content="/images/signoz-hero-image.webp"
content="[[.BaseHref]]images/signoz-hero-image.webp"
/>
<meta
data-react-helmet="true"
@@ -59,7 +60,7 @@
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
<meta name="robots" content="noindex" />
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
<link data-react-helmet="true" rel="shortcut icon" href="favicon.ico" />
</head>
<body data-theme="default">
<script>
@@ -136,7 +137,7 @@
})(document, 'script');
}
</script>
<link rel="stylesheet" href="/css/uPlot.min.css" />
<link rel="stylesheet" href="css/uPlot.min.css" />
<script type="module" src="./src/index.tsx"></script>
</body>
</html>

View File

@@ -48,22 +48,22 @@
"@radix-ui/react-tooltip": "1.0.7",
"@sentry/react": "8.41.0",
"@sentry/vite-plugin": "2.22.6",
"@signozhq/button": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/checkbox": "0.0.2",
"@signozhq/combobox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/button": "0.0.5",
"@signozhq/calendar": "0.1.1",
"@signozhq/callout": "0.0.4",
"@signozhq/checkbox": "0.0.4",
"@signozhq/combobox": "0.0.4",
"@signozhq/command": "0.0.2",
"@signozhq/design-tokens": "2.1.4",
"@signozhq/dialog": "^0.0.2",
"@signozhq/drawer": "0.0.4",
"@signozhq/dialog": "0.0.4",
"@signozhq/drawer": "0.0.6",
"@signozhq/icons": "0.1.0",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",
"@signozhq/radio-group": "0.0.2",
"@signozhq/resizable": "0.0.0",
"@signozhq/input": "0.0.4",
"@signozhq/popover": "0.1.2",
"@signozhq/radio-group": "0.0.4",
"@signozhq/resizable": "0.0.2",
"@signozhq/table": "0.3.7",
"@signozhq/toggle-group": "0.0.1",
"@signozhq/toggle-group": "0.0.3",
"@signozhq/ui": "0.0.5",
"@tanstack/react-table": "8.21.3",
"@tanstack/react-virtual": "3.13.22",

View File

@@ -2,6 +2,7 @@ import { initReactI18next } from 'react-i18next';
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { getBasePath } from 'utils/basePath';
import cacheBursting from '../../i18n-translations-hash.json';
@@ -24,7 +25,7 @@ i18n
const ns = namespace[0];
const pathkey = `/${language}/${ns}`;
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
return `/locales/${language}/${namespace}.json?h=${hash}`;
return `${getBasePath()}locales/${language}/${namespace}.json?h=${hash}`;
},
},
react: {

View File

@@ -0,0 +1,496 @@
/**
* ! Do not edit manually
* * The file has been auto-generated using Orval for SigNoz
* * regenerate with 'yarn generate:api'
* SigNoz
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
CreateDowntimeSchedule201,
DeleteDowntimeScheduleByIDPathParameters,
GetDowntimeScheduleByID200,
GetDowntimeScheduleByIDPathParameters,
ListDowntimeSchedules200,
ListDowntimeSchedulesParams,
RenderErrorResponseDTO,
RuletypesPostablePlannedMaintenanceDTO,
UpdateDowntimeScheduleByIDPathParameters,
} from '../sigNoz.schemas';
/**
* This endpoint lists all planned maintenance / downtime schedules
* @summary List downtime schedules
*/
export const listDowntimeSchedules = (
params?: ListDowntimeSchedulesParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListDowntimeSchedules200>({
url: `/api/v1/downtime_schedules`,
method: 'GET',
params,
signal,
});
};
export const getListDowntimeSchedulesQueryKey = (
params?: ListDowntimeSchedulesParams,
) => {
return [`/api/v1/downtime_schedules`, ...(params ? [params] : [])] as const;
};
export const getListDowntimeSchedulesQueryOptions = <
TData = Awaited<ReturnType<typeof listDowntimeSchedules>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
params?: ListDowntimeSchedulesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listDowntimeSchedules>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getListDowntimeSchedulesQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listDowntimeSchedules>>
> = ({ signal }) => listDowntimeSchedules(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listDowntimeSchedules>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListDowntimeSchedulesQueryResult = NonNullable<
Awaited<ReturnType<typeof listDowntimeSchedules>>
>;
export type ListDowntimeSchedulesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List downtime schedules
*/
export function useListDowntimeSchedules<
TData = Awaited<ReturnType<typeof listDowntimeSchedules>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
params?: ListDowntimeSchedulesParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listDowntimeSchedules>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListDowntimeSchedulesQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List downtime schedules
*/
export const invalidateListDowntimeSchedules = async (
queryClient: QueryClient,
params?: ListDowntimeSchedulesParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListDowntimeSchedulesQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint creates a new planned maintenance / downtime schedule
* @summary Create downtime schedule
*/
export const createDowntimeSchedule = (
ruletypesPostablePlannedMaintenanceDTO: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateDowntimeSchedule201>({
url: `/api/v1/downtime_schedules`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostablePlannedMaintenanceDTO,
signal,
});
};
export const getCreateDowntimeScheduleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
TContext
> => {
const mutationKey = ['createDowntimeSchedule'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
{ data: BodyType<RuletypesPostablePlannedMaintenanceDTO> }
> = (props) => {
const { data } = props ?? {};
return createDowntimeSchedule(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateDowntimeScheduleMutationResult = NonNullable<
Awaited<ReturnType<typeof createDowntimeSchedule>>
>;
export type CreateDowntimeScheduleMutationBody = BodyType<RuletypesPostablePlannedMaintenanceDTO>;
export type CreateDowntimeScheduleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create downtime schedule
*/
export const useCreateDowntimeSchedule = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createDowntimeSchedule>>,
TError,
{ data: BodyType<RuletypesPostablePlannedMaintenanceDTO> },
TContext
> => {
const mutationOptions = getCreateDowntimeScheduleMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes a downtime schedule by ID
* @summary Delete downtime schedule
*/
export const deleteDowntimeScheduleByID = ({
id,
}: DeleteDowntimeScheduleByIDPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/downtime_schedules/${id}`,
method: 'DELETE',
});
};
export const getDeleteDowntimeScheduleByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
TError,
{ pathParams: DeleteDowntimeScheduleByIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
TError,
{ pathParams: DeleteDowntimeScheduleByIDPathParameters },
TContext
> => {
const mutationKey = ['deleteDowntimeScheduleByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
{ pathParams: DeleteDowntimeScheduleByIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteDowntimeScheduleByID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteDowntimeScheduleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>
>;
export type DeleteDowntimeScheduleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete downtime schedule
*/
export const useDeleteDowntimeScheduleByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
TError,
{ pathParams: DeleteDowntimeScheduleByIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteDowntimeScheduleByID>>,
TError,
{ pathParams: DeleteDowntimeScheduleByIDPathParameters },
TContext
> => {
const mutationOptions = getDeleteDowntimeScheduleByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint returns a downtime schedule by ID
* @summary Get downtime schedule by ID
*/
export const getDowntimeScheduleByID = (
{ id }: GetDowntimeScheduleByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetDowntimeScheduleByID200>({
url: `/api/v1/downtime_schedules/${id}`,
method: 'GET',
signal,
});
};
export const getGetDowntimeScheduleByIDQueryKey = ({
id,
}: GetDowntimeScheduleByIDPathParameters) => {
return [`/api/v1/downtime_schedules/${id}`] as const;
};
export const getGetDowntimeScheduleByIDQueryOptions = <
TData = Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetDowntimeScheduleByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetDowntimeScheduleByIDQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getDowntimeScheduleByID>>
> = ({ signal }) => getDowntimeScheduleByID({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetDowntimeScheduleByIDQueryResult = NonNullable<
Awaited<ReturnType<typeof getDowntimeScheduleByID>>
>;
export type GetDowntimeScheduleByIDQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get downtime schedule by ID
*/
export function useGetDowntimeScheduleByID<
TData = Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetDowntimeScheduleByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getDowntimeScheduleByID>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetDowntimeScheduleByIDQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get downtime schedule by ID
*/
export const invalidateGetDowntimeScheduleByID = async (
queryClient: QueryClient,
{ id }: GetDowntimeScheduleByIDPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetDowntimeScheduleByIDQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint updates a downtime schedule by ID
* @summary Update downtime schedule
*/
export const updateDowntimeScheduleByID = (
{ id }: UpdateDowntimeScheduleByIDPathParameters,
ruletypesPostablePlannedMaintenanceDTO: BodyType<RuletypesPostablePlannedMaintenanceDTO>,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/downtime_schedules/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostablePlannedMaintenanceDTO,
});
};
export const getUpdateDowntimeScheduleByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
},
TContext
> => {
const mutationKey = ['updateDowntimeScheduleByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateDowntimeScheduleByID(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateDowntimeScheduleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>
>;
export type UpdateDowntimeScheduleByIDMutationBody = BodyType<RuletypesPostablePlannedMaintenanceDTO>;
export type UpdateDowntimeScheduleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update downtime schedule
*/
export const useUpdateDowntimeScheduleByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateDowntimeScheduleByID>>,
TError,
{
pathParams: UpdateDowntimeScheduleByIDPathParameters;
data: BodyType<RuletypesPostablePlannedMaintenanceDTO>;
},
TContext
> => {
const mutationOptions = getUpdateDowntimeScheduleByIDMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -6,17 +6,24 @@
*/
import type {
InvalidateOptions,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from 'react-query';
import { useQuery } from 'react-query';
import { useMutation, useQuery } from 'react-query';
import type { ErrorType } from '../../../generatedAPIInstance';
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
CreateRule201,
DeleteRuleByIDPathParameters,
GetRuleByID200,
GetRuleByIDPathParameters,
GetRuleHistoryFilterKeys200,
GetRuleHistoryFilterKeysParams,
GetRuleHistoryFilterKeysPathParameters,
@@ -35,9 +42,548 @@ import type {
GetRuleHistoryTopContributors200,
GetRuleHistoryTopContributorsParams,
GetRuleHistoryTopContributorsPathParameters,
ListRules200,
PatchRuleByID200,
PatchRuleByIDPathParameters,
RenderErrorResponseDTO,
RuletypesPostableRuleDTO,
TestRule200,
UpdateRuleByIDPathParameters,
} from '../sigNoz.schemas';
/**
* This endpoint lists all alert rules with their current evaluation state
* @summary List alert rules
*/
export const listRules = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListRules200>({
url: `/api/v2/rules`,
method: 'GET',
signal,
});
};
export const getListRulesQueryKey = () => {
return [`/api/v2/rules`] as const;
};
export const getListRulesQueryOptions = <
TData = Awaited<ReturnType<typeof listRules>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof listRules>>, TError, TData>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListRulesQueryKey();
const queryFn: QueryFunction<Awaited<ReturnType<typeof listRules>>> = ({
signal,
}) => listRules(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listRules>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListRulesQueryResult = NonNullable<
Awaited<ReturnType<typeof listRules>>
>;
export type ListRulesQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List alert rules
*/
export function useListRules<
TData = Awaited<ReturnType<typeof listRules>>,
TError = ErrorType<RenderErrorResponseDTO>
>(options?: {
query?: UseQueryOptions<Awaited<ReturnType<typeof listRules>>, TError, TData>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListRulesQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary List alert rules
*/
export const invalidateListRules = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListRulesQueryKey() },
options,
);
return queryClient;
};
/**
* This endpoint creates a new alert rule
* @summary Create alert rule
*/
export const createRule = (
ruletypesPostableRuleDTO: BodyType<RuletypesPostableRuleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRule201>({
url: `/api/v2/rules`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostableRuleDTO,
signal,
});
};
export const getCreateRuleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRule>>,
TError,
{ data: BodyType<RuletypesPostableRuleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRule>>,
TError,
{ data: BodyType<RuletypesPostableRuleDTO> },
TContext
> => {
const mutationKey = ['createRule'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRule>>,
{ data: BodyType<RuletypesPostableRuleDTO> }
> = (props) => {
const { data } = props ?? {};
return createRule(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateRuleMutationResult = NonNullable<
Awaited<ReturnType<typeof createRule>>
>;
export type CreateRuleMutationBody = BodyType<RuletypesPostableRuleDTO>;
export type CreateRuleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create alert rule
*/
export const useCreateRule = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRule>>,
TError,
{ data: BodyType<RuletypesPostableRuleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRule>>,
TError,
{ data: BodyType<RuletypesPostableRuleDTO> },
TContext
> => {
const mutationOptions = getCreateRuleMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint deletes an alert rule by ID
* @summary Delete alert rule
*/
export const deleteRuleByID = ({ id }: DeleteRuleByIDPathParameters) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/rules/${id}`,
method: 'DELETE',
});
};
export const getDeleteRuleByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRuleByID>>,
TError,
{ pathParams: DeleteRuleByIDPathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteRuleByID>>,
TError,
{ pathParams: DeleteRuleByIDPathParameters },
TContext
> => {
const mutationKey = ['deleteRuleByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteRuleByID>>,
{ pathParams: DeleteRuleByIDPathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteRuleByID(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteRuleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteRuleByID>>
>;
export type DeleteRuleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete alert rule
*/
export const useDeleteRuleByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteRuleByID>>,
TError,
{ pathParams: DeleteRuleByIDPathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteRuleByID>>,
TError,
{ pathParams: DeleteRuleByIDPathParameters },
TContext
> => {
const mutationOptions = getDeleteRuleByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint returns an alert rule by ID
* @summary Get alert rule by ID
*/
export const getRuleByID = (
{ id }: GetRuleByIDPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetRuleByID200>({
url: `/api/v2/rules/${id}`,
method: 'GET',
signal,
});
};
export const getGetRuleByIDQueryKey = ({ id }: GetRuleByIDPathParameters) => {
return [`/api/v2/rules/${id}`] as const;
};
export const getGetRuleByIDQueryOptions = <
TData = Awaited<ReturnType<typeof getRuleByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleByID>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetRuleByIDQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getRuleByID>>> = ({
signal,
}) => getRuleByID({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getRuleByID>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetRuleByIDQueryResult = NonNullable<
Awaited<ReturnType<typeof getRuleByID>>
>;
export type GetRuleByIDQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get alert rule by ID
*/
export function useGetRuleByID<
TData = Awaited<ReturnType<typeof getRuleByID>>,
TError = ErrorType<RenderErrorResponseDTO>
>(
{ id }: GetRuleByIDPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getRuleByID>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetRuleByIDQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
/**
* @summary Get alert rule by ID
*/
export const invalidateGetRuleByID = async (
queryClient: QueryClient,
{ id }: GetRuleByIDPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetRuleByIDQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint applies a partial update to an alert rule by ID
* @summary Patch alert rule
*/
export const patchRuleByID = (
{ id }: PatchRuleByIDPathParameters,
ruletypesPostableRuleDTO: BodyType<RuletypesPostableRuleDTO>,
) => {
return GeneratedAPIInstance<PatchRuleByID200>({
url: `/api/v2/rules/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostableRuleDTO,
});
};
export const getPatchRuleByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchRuleByID>>,
TError,
{
pathParams: PatchRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof patchRuleByID>>,
TError,
{
pathParams: PatchRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
TContext
> => {
const mutationKey = ['patchRuleByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof patchRuleByID>>,
{
pathParams: PatchRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return patchRuleByID(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type PatchRuleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof patchRuleByID>>
>;
export type PatchRuleByIDMutationBody = BodyType<RuletypesPostableRuleDTO>;
export type PatchRuleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Patch alert rule
*/
export const usePatchRuleByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof patchRuleByID>>,
TError,
{
pathParams: PatchRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof patchRuleByID>>,
TError,
{
pathParams: PatchRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
TContext
> => {
const mutationOptions = getPatchRuleByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* This endpoint updates an alert rule by ID
* @summary Update alert rule
*/
export const updateRuleByID = (
{ id }: UpdateRuleByIDPathParameters,
ruletypesPostableRuleDTO: BodyType<RuletypesPostableRuleDTO>,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/rules/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostableRuleDTO,
});
};
export const getUpdateRuleByIDMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRuleByID>>,
TError,
{
pathParams: UpdateRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateRuleByID>>,
TError,
{
pathParams: UpdateRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
TContext
> => {
const mutationKey = ['updateRuleByID'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateRuleByID>>,
{
pathParams: UpdateRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return updateRuleByID(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateRuleByIDMutationResult = NonNullable<
Awaited<ReturnType<typeof updateRuleByID>>
>;
export type UpdateRuleByIDMutationBody = BodyType<RuletypesPostableRuleDTO>;
export type UpdateRuleByIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Update alert rule
*/
export const useUpdateRuleByID = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateRuleByID>>,
TError,
{
pathParams: UpdateRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateRuleByID>>,
TError,
{
pathParams: UpdateRuleByIDPathParameters;
data: BodyType<RuletypesPostableRuleDTO>;
},
TContext
> => {
const mutationOptions = getUpdateRuleByIDMutationOptions(options);
return useMutation(mutationOptions);
};
/**
* Returns distinct label keys from rule history entries for the selected range.
* @summary Get rule history filter keys
@@ -742,3 +1288,87 @@ export const invalidateGetRuleHistoryTopContributors = async (
return queryClient;
};
/**
* This endpoint fires a test notification for the given rule definition
* @summary Test alert rule
*/
export const testRule = (
ruletypesPostableRuleDTO: BodyType<RuletypesPostableRuleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<TestRule200>({
url: `/api/v2/rules/test`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: ruletypesPostableRuleDTO,
signal,
});
};
export const getTestRuleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testRule>>,
TError,
{ data: BodyType<RuletypesPostableRuleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof testRule>>,
TError,
{ data: BodyType<RuletypesPostableRuleDTO> },
TContext
> => {
const mutationKey = ['testRule'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof testRule>>,
{ data: BodyType<RuletypesPostableRuleDTO> }
> = (props) => {
const { data } = props ?? {};
return testRule(data);
};
return { mutationFn, ...mutationOptions };
};
export type TestRuleMutationResult = NonNullable<
Awaited<ReturnType<typeof testRule>>
>;
export type TestRuleMutationBody = BodyType<RuletypesPostableRuleDTO>;
export type TestRuleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Test alert rule
*/
export const useTestRule = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof testRule>>,
TError,
{ data: BodyType<RuletypesPostableRuleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof testRule>>,
TError,
{ data: BodyType<RuletypesPostableRuleDTO> },
TContext
> => {
const mutationOptions = getTestRuleMutationOptions(options);
return useMutation(mutationOptions);
};

View File

@@ -4529,6 +4529,20 @@ export interface RulestatehistorytypesGettableRuleStateWindowDTO {
state: RuletypesAlertStateDTO;
}
export interface RuletypesAlertCompositeQueryDTO {
panelType: RuletypesPanelTypeDTO;
/**
* @type array
* @nullable true
*/
queries: Querybuildertypesv5QueryEnvelopeDTO[] | null;
queryType: RuletypesQueryTypeDTO;
/**
* @type string
*/
unit?: string;
}
export enum RuletypesAlertStateDTO {
inactive = 'inactive',
pending = 'pending',
@@ -4537,6 +4551,513 @@ export enum RuletypesAlertStateDTO {
nodata = 'nodata',
disabled = 'disabled',
}
export enum RuletypesAlertTypeDTO {
METRIC_BASED_ALERT = 'METRIC_BASED_ALERT',
TRACES_BASED_ALERT = 'TRACES_BASED_ALERT',
LOGS_BASED_ALERT = 'LOGS_BASED_ALERT',
EXCEPTIONS_BASED_ALERT = 'EXCEPTIONS_BASED_ALERT',
}
export interface RuletypesBasicRuleThresholdDTO {
/**
* @type array
* @nullable true
*/
channels?: string[] | null;
matchType: RuletypesMatchTypeDTO;
/**
* @type string
*/
name: string;
op: RuletypesCompareOperatorDTO;
/**
* @type number
* @nullable true
*/
recoveryTarget?: number | null;
/**
* @type number
* @nullable true
*/
target: number | null;
/**
* @type string
*/
targetUnit?: string;
}
/**
* @nullable
*/
export type RuletypesBasicRuleThresholdsDTO =
| RuletypesBasicRuleThresholdDTO[]
| null;
export enum RuletypesCompareOperatorDTO {
above = 'above',
below = 'below',
equal = 'equal',
not_equal = 'not_equal',
outside_bounds = 'outside_bounds',
}
export interface RuletypesCumulativeScheduleDTO {
/**
* @type integer
* @nullable true
*/
day?: number | null;
/**
* @type integer
* @nullable true
*/
hour?: number | null;
/**
* @type integer
* @nullable true
*/
minute?: number | null;
type: RuletypesScheduleTypeDTO;
/**
* @type integer
* @nullable true
*/
weekday?: number | null;
}
export interface RuletypesCumulativeWindowDTO {
/**
* @type string
*/
frequency: string;
schedule: RuletypesCumulativeScheduleDTO;
/**
* @type string
*/
timezone: string;
}
export interface RuletypesEvaluationCumulativeDTO {
kind?: RuletypesEvaluationKindDTO;
spec?: RuletypesCumulativeWindowDTO;
}
export type RuletypesEvaluationEnvelopeDTO =
| (RuletypesEvaluationRollingDTO & {
kind: RuletypesEvaluationKindDTO;
spec: unknown;
})
| (RuletypesEvaluationCumulativeDTO & {
kind: RuletypesEvaluationKindDTO;
spec: unknown;
});
export enum RuletypesEvaluationKindDTO {
rolling = 'rolling',
cumulative = 'cumulative',
}
export interface RuletypesEvaluationRollingDTO {
kind?: RuletypesEvaluationKindDTO;
spec?: RuletypesRollingWindowDTO;
}
export interface RuletypesGettableTestRuleDTO {
/**
* @type integer
*/
alertCount?: number;
/**
* @type string
*/
message?: string;
}
export enum RuletypesMaintenanceKindDTO {
fixed = 'fixed',
recurring = 'recurring',
}
export enum RuletypesMaintenanceStatusDTO {
active = 'active',
upcoming = 'upcoming',
expired = 'expired',
}
export enum RuletypesMatchTypeDTO {
at_least_once = 'at_least_once',
all_the_times = 'all_the_times',
on_average = 'on_average',
in_total = 'in_total',
last = 'last',
}
export interface RuletypesNotificationSettingsDTO {
/**
* @type array
*/
groupBy?: string[];
/**
* @type string
*/
newGroupEvalDelay?: string;
renotify?: RuletypesRenotifyDTO;
/**
* @type boolean
*/
usePolicy?: boolean;
}
export enum RuletypesPanelTypeDTO {
value = 'value',
table = 'table',
graph = 'graph',
}
export interface RuletypesPlannedMaintenanceDTO {
/**
* @type array
* @nullable true
*/
alertIds?: string[] | null;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
id: string;
kind: RuletypesMaintenanceKindDTO;
/**
* @type string
*/
name: string;
schedule: RuletypesScheduleDTO;
status: RuletypesMaintenanceStatusDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
}
export interface RuletypesPostablePlannedMaintenanceDTO {
/**
* @type array
* @nullable true
*/
alertIds?: string[] | null;
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
schedule: RuletypesScheduleDTO;
}
export type RuletypesPostableRuleDTOAnnotations = { [key: string]: string };
export type RuletypesPostableRuleDTOLabels = { [key: string]: string };
export interface RuletypesPostableRuleDTO {
/**
* @type string
*/
alert: string;
alertType?: RuletypesAlertTypeDTO;
/**
* @type object
*/
annotations?: RuletypesPostableRuleDTOAnnotations;
condition: RuletypesRuleConditionDTO;
/**
* @type string
*/
description?: string;
/**
* @type boolean
*/
disabled?: boolean;
/**
* @type string
*/
evalWindow?: string;
evaluation?: RuletypesEvaluationEnvelopeDTO;
/**
* @type string
*/
frequency?: string;
/**
* @type object
*/
labels?: RuletypesPostableRuleDTOLabels;
notificationSettings?: RuletypesNotificationSettingsDTO;
/**
* @type array
*/
preferredChannels?: string[];
ruleType: RuletypesRuleTypeDTO;
/**
* @type string
*/
schemaVersion?: string;
/**
* @type string
*/
source?: string;
/**
* @type string
*/
version?: string;
}
export enum RuletypesQueryTypeDTO {
builder = 'builder',
clickhouse_sql = 'clickhouse_sql',
promql = 'promql',
}
export interface RuletypesRecurrenceDTO {
/**
* @type string
*/
duration: string;
/**
* @type string
* @format date-time
* @nullable true
*/
endTime?: Date | null;
/**
* @type array
* @nullable true
*/
repeatOn?: RuletypesRepeatOnDTO[] | null;
repeatType: RuletypesRepeatTypeDTO;
/**
* @type string
* @format date-time
*/
startTime: Date;
}
export interface RuletypesRenotifyDTO {
/**
* @type array
*/
alertStates?: RuletypesAlertStateDTO[];
/**
* @type boolean
*/
enabled?: boolean;
/**
* @type string
*/
interval?: string;
}
export enum RuletypesRepeatOnDTO {
sunday = 'sunday',
monday = 'monday',
tuesday = 'tuesday',
wednesday = 'wednesday',
thursday = 'thursday',
friday = 'friday',
saturday = 'saturday',
}
export enum RuletypesRepeatTypeDTO {
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export interface RuletypesRollingWindowDTO {
/**
* @type string
*/
evalWindow: string;
/**
* @type string
*/
frequency: string;
}
export type RuletypesRuleDTOAnnotations = { [key: string]: string };
export type RuletypesRuleDTOLabels = { [key: string]: string };
export interface RuletypesRuleDTO {
/**
* @type string
*/
alert: string;
alertType?: RuletypesAlertTypeDTO;
/**
* @type object
*/
annotations?: RuletypesRuleDTOAnnotations;
condition: RuletypesRuleConditionDTO;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
createdBy?: string;
/**
* @type string
*/
description?: string;
/**
* @type boolean
*/
disabled?: boolean;
/**
* @type string
*/
evalWindow?: string;
evaluation?: RuletypesEvaluationEnvelopeDTO;
/**
* @type string
*/
frequency?: string;
/**
* @type string
*/
id: string;
/**
* @type object
*/
labels?: RuletypesRuleDTOLabels;
notificationSettings?: RuletypesNotificationSettingsDTO;
/**
* @type array
*/
preferredChannels?: string[];
ruleType: RuletypesRuleTypeDTO;
/**
* @type string
*/
schemaVersion?: string;
/**
* @type string
*/
source?: string;
state: RuletypesAlertStateDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
/**
* @type string
*/
updatedBy?: string;
/**
* @type string
*/
version?: string;
}
export interface RuletypesRuleConditionDTO {
/**
* @type integer
* @minimum 0
*/
absentFor?: number;
/**
* @type boolean
*/
alertOnAbsent?: boolean;
/**
* @type string
*/
algorithm?: string;
compositeQuery: RuletypesAlertCompositeQueryDTO;
matchType: RuletypesMatchTypeDTO;
op: RuletypesCompareOperatorDTO;
/**
* @type boolean
*/
requireMinPoints?: boolean;
/**
* @type integer
*/
requiredNumPoints?: number;
seasonality?: RuletypesSeasonalityDTO;
/**
* @type string
*/
selectedQueryName?: string;
/**
* @type number
* @nullable true
*/
target?: number | null;
/**
* @type string
*/
targetUnit?: string;
thresholds?: RuletypesRuleThresholdDataDTO;
}
export type RuletypesRuleThresholdDataDTO = RuletypesThresholdBasicDTO & {
kind: RuletypesThresholdKindDTO;
spec: unknown;
};
export enum RuletypesRuleTypeDTO {
threshold_rule = 'threshold_rule',
promql_rule = 'promql_rule',
anomaly_rule = 'anomaly_rule',
}
export interface RuletypesScheduleDTO {
/**
* @type string
* @format date-time
*/
endTime?: Date;
recurrence?: RuletypesRecurrenceDTO;
/**
* @type string
* @format date-time
*/
startTime?: Date;
/**
* @type string
*/
timezone: string;
}
export enum RuletypesScheduleTypeDTO {
hourly = 'hourly',
daily = 'daily',
weekly = 'weekly',
monthly = 'monthly',
}
export enum RuletypesSeasonalityDTO {
hourly = 'hourly',
daily = 'daily',
weekly = 'weekly',
}
export interface RuletypesThresholdBasicDTO {
kind?: RuletypesThresholdKindDTO;
spec?: RuletypesBasicRuleThresholdsDTO;
}
export enum RuletypesThresholdKindDTO {
basic = 'basic',
}
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
/**
* @type string
@@ -5456,6 +5977,57 @@ export type DeleteAuthDomainPathParameters = {
export type UpdateAuthDomainPathParameters = {
id: string;
};
export type ListDowntimeSchedulesParams = {
/**
* @type boolean
* @nullable true
* @description undefined
*/
active?: boolean | null;
/**
* @type boolean
* @nullable true
* @description undefined
*/
recurring?: boolean | null;
};
export type ListDowntimeSchedules200 = {
/**
* @type array
*/
data: RuletypesPlannedMaintenanceDTO[];
/**
* @type string
*/
status: string;
};
export type CreateDowntimeSchedule201 = {
data: RuletypesPlannedMaintenanceDTO;
/**
* @type string
*/
status: string;
};
export type DeleteDowntimeScheduleByIDPathParameters = {
id: string;
};
export type GetDowntimeScheduleByIDPathParameters = {
id: string;
};
export type GetDowntimeScheduleByID200 = {
data: RuletypesPlannedMaintenanceDTO;
/**
* @type string
*/
status: string;
};
export type UpdateDowntimeScheduleByIDPathParameters = {
id: string;
};
export type HandleExportRawDataPOSTParams = {
/**
* @enum csv,jsonl
@@ -6253,6 +6825,53 @@ export type GetUsersByRoleID200 = {
status: string;
};
export type ListRules200 = {
/**
* @type array
*/
data: RuletypesRuleDTO[];
/**
* @type string
*/
status: string;
};
export type CreateRule201 = {
data: RuletypesRuleDTO;
/**
* @type string
*/
status: string;
};
export type DeleteRuleByIDPathParameters = {
id: string;
};
export type GetRuleByIDPathParameters = {
id: string;
};
export type GetRuleByID200 = {
data: RuletypesRuleDTO;
/**
* @type string
*/
status: string;
};
export type PatchRuleByIDPathParameters = {
id: string;
};
export type PatchRuleByID200 = {
data: RuletypesRuleDTO;
/**
* @type string
*/
status: string;
};
export type UpdateRuleByIDPathParameters = {
id: string;
};
export type GetRuleHistoryFilterKeysPathParameters = {
id: string;
};
@@ -6523,6 +7142,14 @@ export type GetRuleHistoryTopContributors200 = {
status: string;
};
export type TestRule200 = {
data: RuletypesGettableTestRuleDTO;
/**
* @type string
*/
status: string;
};
export type GetSessionContext200 = {
data: AuthtypesSessionContextDTO;
/**

View File

@@ -1,5 +1,6 @@
import {
interceptorRejected,
interceptorsRequestBasePath,
interceptorsRequestResponse,
interceptorsResponse,
} from 'api';
@@ -17,6 +18,7 @@ export const GeneratedAPIInstance = <T>(
return generatedAPIAxiosInstance({ ...config }).then(({ data }) => data);
};
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
generatedAPIAxiosInstance.interceptors.response.use(
interceptorsResponse,

View File

@@ -11,6 +11,7 @@ import axios, {
import { ENVIRONMENT } from 'constants/env';
import { Events } from 'constants/events';
import { LOCALSTORAGE } from 'constants/localStorage';
import { getBasePath } from 'utils/basePath';
import { eventEmitter } from 'utils/getEventEmitter';
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
@@ -67,6 +68,39 @@ export const interceptorsRequestResponse = (
return value;
};
// Strips the leading '/' from path and joins with base — idempotent if already prefixed.
// e.g. prependBase('/signoz/', '/api/v1/') → '/signoz/api/v1/'
function prependBase(base: string, path: string): string {
return path.startsWith(base) ? path : base + path.slice(1);
}
// Prepends the runtime base path to outgoing requests so API calls work under
// a URL prefix (e.g. /signoz/api/v1/…). No-op for root deployments and dev
// (dev baseURL is a full http:// URL, not an absolute path).
export const interceptorsRequestBasePath = (
value: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig => {
const basePath = getBasePath();
if (basePath === '/') {
return value;
}
if (value.baseURL?.startsWith('/')) {
// Production relative baseURL: '/api/v1/' → '/signoz/api/v1/'
value.baseURL = prependBase(basePath, value.baseURL);
} else if (value.baseURL?.startsWith('http')) {
// Dev absolute baseURL (VITE_FRONTEND_API_ENDPOINT): 'https://host/api/v1/' → 'https://host/signoz/api/v1/'
const url = new URL(value.baseURL);
url.pathname = prependBase(basePath, url.pathname);
value.baseURL = url.toString();
} else if (!value.baseURL && value.url?.startsWith('/')) {
// Orval-generated client (empty baseURL, path in url): '/api/signoz/v1/rules' → '/signoz/api/signoz/v1/rules'
value.url = prependBase(basePath, value.url);
}
return value;
};
export const interceptorRejected = async (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => {
@@ -133,6 +167,7 @@ const instance = axios.create({
});
instance.interceptors.request.use(interceptorsRequestResponse);
instance.interceptors.request.use(interceptorsRequestBasePath);
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
export const AxiosAlertManagerInstance = axios.create({
@@ -147,6 +182,7 @@ ApiV2Instance.interceptors.response.use(
interceptorRejected,
);
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV2Instance.interceptors.request.use(interceptorsRequestBasePath);
// axios V3
export const ApiV3Instance = axios.create({
@@ -158,6 +194,7 @@ ApiV3Instance.interceptors.response.use(
interceptorRejected,
);
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV3Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V4
@@ -170,6 +207,7 @@ ApiV4Instance.interceptors.response.use(
interceptorRejected,
);
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV4Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios V5
@@ -182,6 +220,7 @@ ApiV5Instance.interceptors.response.use(
interceptorRejected,
);
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
ApiV5Instance.interceptors.request.use(interceptorsRequestBasePath);
//
// axios Base
@@ -194,6 +233,7 @@ LogEventAxiosInstance.interceptors.response.use(
interceptorRejectedBase,
);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
//
AxiosAlertManagerInstance.interceptors.response.use(
@@ -201,6 +241,7 @@ AxiosAlertManagerInstance.interceptors.response.use(
interceptorRejected,
);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestResponse);
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestBasePath);
export { apiV1 };
export default instance;

View File

@@ -42,6 +42,7 @@
height: 32px;
padding: 10px 16px;
background: var(--l2-background);
color: var(--l2-foreground);
border: none;
border-radius: 2px;
cursor: pointer;
@@ -65,10 +66,3 @@
opacity: 0.8;
}
}
.lightMode {
.auth-header-help-button {
background: var(--l2-background);
border: 1px solid var(--l1-border);
}
}

View File

@@ -12,6 +12,7 @@ import { AppState } from 'store/reducers';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { withBasePath } from 'utils/basePath';
export interface NavigateToExplorerProps {
filters: TagFilterItem[];
@@ -133,7 +134,7 @@ export function useNavigateToExplorer(): (
QueryParams.compositeQuery
}=${JSONCompositeQuery}`;
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
},
[
prepareQuery,

View File

@@ -9,6 +9,7 @@ import { CreditCard, MessageSquareText, X } from 'lucide-react';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
export default function ChatSupportGateway(): JSX.Element {
const { notifications } = useNotifications();
@@ -54,7 +55,7 @@ export default function ChatSupportGateway(): JSX.Element {
});
updateCreditCard({
url: window.location.origin,
url: getBaseUrl(),
});
};

View File

@@ -31,6 +31,7 @@ import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { useTimezone } from 'providers/Timezone';
import APIError from 'types/api/error';
import { getAbsoluteUrl } from 'utils/basePath';
import { toAPIError } from 'utils/errorUtils';
import DeleteMemberDialog from './DeleteMemberDialog';
@@ -387,7 +388,7 @@ function EditMemberDrawer({
pathParams: { id: member.id },
});
if (response?.data?.token) {
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
const link = getAbsoluteUrl(`/password-reset?token=${response.data.token}`);
setResetLink(link);
setResetLinkExpiresAt(
response.data.expiresAt

View File

@@ -13,6 +13,7 @@ import GetMinMax from 'lib/getMinMax';
import { Check, Info, Link2 } from 'lucide-react';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getAbsoluteUrl } from 'utils/basePath';
const routesToBeSharedWithTime = [
ROUTES.LOGS_EXPLORER,
@@ -80,17 +81,13 @@ function ShareURLModal(): JSX.Element {
urlQuery.delete(QueryParams.relativeTime);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
} else {
urlQuery.delete(QueryParams.startTime);
urlQuery.delete(QueryParams.endTime);
urlQuery.set(QueryParams.relativeTime, selectedTime);
currentUrl = `${window.location.origin}${
location.pathname
}?${urlQuery.toString()}`;
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
}
}

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import { VIEW_TYPES, VIEWS } from './constants';
@@ -330,10 +331,7 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -352,10 +350,7 @@ function HostMetricsDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -14,6 +14,7 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { ROLES } from 'types/roles';
import { EMAIL_REGEX } from 'utils/app';
import { getBaseUrl } from 'utils/basePath';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
@@ -188,7 +189,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role as ROLES,
frontendBaseUrl: window.location.origin,
frontendBaseUrl: getBaseUrl(),
});
} else {
await inviteUsers({
@@ -196,7 +197,7 @@ function InviteMembersModal({
email: row.email.trim(),
name: '',
role: row.role,
frontendBaseUrl: window.location.origin,
frontendBaseUrl: getBaseUrl(),
})),
});
}

View File

@@ -14,6 +14,7 @@ import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import './LaunchChatSupport.styles.scss';
@@ -154,7 +155,7 @@ function LaunchChatSupport({
});
updateCreditCard({
url: window.location.origin,
url: getBaseUrl(),
});
};

View File

@@ -1,6 +1,7 @@
import { Color } from '@signozhq/design-tokens';
import { Button } from 'antd';
import { ArrowUpRight } from 'lucide-react';
import { openInNewTab } from 'utils/navigation';
import './LearnMore.styles.scss';
@@ -14,7 +15,7 @@ function LearnMore({ text, url, onClick }: LearnMoreProps): JSX.Element {
const handleClick = (): void => {
onClick?.();
if (url) {
window.open(url, '_blank');
openInNewTab(url);
}
};
return (

View File

@@ -20,6 +20,7 @@ import {
KAFKA_SETUP_DOC_LINK,
MessagingQueueHealthCheckService,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuid } from 'uuid';
import './MessagingQueueHealthCheck.styles.scss';
@@ -76,7 +77,7 @@ function ErrorTitleAndKey({
if (isCloudUserVal && !!link) {
history.push(link);
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
openInNewTab(KAFKA_SETUP_DOC_LINK);
}
};
return {

View File

@@ -46,8 +46,8 @@
}
&__button {
background: var(--card);
color: var(--accent-primary);
background: var(--secondary-background);
color: var(--secondary-foreground);
border: none;
padding: 6px 12px;
border-radius: 4px;

View File

@@ -9,6 +9,7 @@ import {
} from 'container/ApiMonitoring/utils';
import { UnfoldVertical } from 'lucide-react';
import { SuccessResponse } from 'types/api';
import { openInNewTab } from 'utils/navigation';
import emptyStateUrl from '@/assets/Icons/emptyState.svg';
@@ -94,20 +95,14 @@ function DependentServices({
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
const url = new URL(
`/services/${
record.serviceData.serviceName &&
record.serviceData.serviceName !== '-'
? record.serviceData.serviceName
: ''
}`,
window.location.origin,
);
const serviceName =
record.serviceData.serviceName && record.serviceData.serviceName !== '-'
? record.serviceData.serviceName
: '';
const urlQuery = new URLSearchParams();
urlQuery.set(QueryParams.startTime, timeRange.startTime.toString());
urlQuery.set(QueryParams.endTime, timeRange.endTime.toString());
url.search = urlQuery.toString();
window.open(url.toString(), '_blank');
openInNewTab(`/services/${serviceName}?${urlQuery.toString()}`);
},
className: 'clickable-row',
})}

View File

@@ -73,6 +73,7 @@ import {
import { UserPreference } from 'types/api/preferences/preference';
import AppReducer from 'types/reducer/app';
import { USER_ROLES } from 'types/roles';
import { getBaseUrl } from 'utils/basePath';
import { showErrorNotification } from 'utils/error';
import { eventEmitter } from 'utils/getEventEmitter';
import {
@@ -461,7 +462,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const handleFailedPayment = useCallback((): void => {
manageCreditCard({
url: window.location.origin,
url: getBaseUrl(),
});
}, [manageCreditCard]);

View File

@@ -31,6 +31,7 @@ import { isEmpty, pick } from 'lodash-es';
import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import { getBaseUrl } from 'utils/basePath';
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
@@ -324,7 +325,7 @@ export default function BillingContainer(): JSX.Element {
});
updateCreditCard({
url: window.location.origin,
url: getBaseUrl(),
});
} else {
logEvent('Billing : Manage Billing', {
@@ -333,7 +334,7 @@ export default function BillingContainer(): JSX.Element {
});
manageCreditCard({
url: window.location.origin,
url: getBaseUrl(),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@@ -7,6 +7,7 @@ import { FeatureKeys } from 'constants/features';
import { useAppContext } from 'providers/App/App';
import { AlertTypes } from 'types/api/alerts/alertTypes';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import { getOptionList } from './config';
import { AlertTypeCard, SelectTypeContainer } from './styles';
@@ -55,7 +56,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
page: 'New alert data source selection page',
});
window.open(url, '_blank');
openInNewTab(url);
}
const renderOptions = useMemo(
() => (

View File

@@ -14,6 +14,7 @@ import { IUser } from 'providers/App/types';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import { ROUTING_POLICIES_ROUTE } from './constants';
import { RoutingPolicyBannerProps } from './types';
@@ -387,7 +388,7 @@ export function NotificationChannelsNotFoundContent({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
window.open(ROUTES.CHANNELS_NEW, '_blank');
openInNewTab(ROUTES.CHANNELS_NEW);
}}
>
here.

View File

@@ -48,6 +48,7 @@ function DomainUpdateToast({
className="custom-domain-toast-visit-btn"
suffixIcon={<ExternalLink size={12} />}
onClick={(): void => {
// eslint-disable-next-line rulesdir/no-raw-absolute-path
window.open(url, '_blank', 'noopener,noreferrer');
}}
>

View File

@@ -16,6 +16,8 @@ import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
import { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import './PublicDashboard.styles.scss';
@@ -213,7 +215,7 @@ function PublicDashboardSetting(): JSX.Element {
try {
setCopyPublicDashboardURL(
`${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
);
toast.success('Copied Public Dashboard URL successfully');
} catch (error) {
@@ -222,7 +224,7 @@ function PublicDashboardSetting(): JSX.Element {
};
const publicDashboardURL = useMemo(
() => `${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
() => getAbsoluteUrl(publicDashboardResponse?.data?.publicPath ?? ''),
[publicDashboardResponse],
);
@@ -294,7 +296,7 @@ function PublicDashboardSetting(): JSX.Element {
icon={<ExternalLink size={12} />}
onClick={(): void => {
if (publicDashboardURL) {
window.open(publicDashboardURL, '_blank');
openInNewTab(publicDashboardURL);
}
}}
/>

View File

@@ -10,6 +10,7 @@ import ROUTES from 'constants/routes';
import history from 'lib/history';
import APIError from 'types/api/error';
import { OrgSessionContext } from 'types/api/v2/sessions/context/get';
import { getBaseUrl } from 'utils/basePath';
import tvUrl from '@/assets/svgs/tv.svg';
@@ -105,7 +106,7 @@ function ForgotPassword({
data: {
email: values.email,
orgId: currentOrgId,
frontendBaseURL: window.location.origin,
frontendBaseURL: getBaseUrl(),
},
});
}, [form, forgotPasswordMutate, initialOrgId, hasMultipleOrgs]);

View File

@@ -15,6 +15,7 @@ import { AlertDef, Labels } from 'types/api/alerts/def';
import { Channels } from 'types/api/channels/getAll';
import APIError from 'types/api/error';
import { requireErrorMessage } from 'utils/form/requireErrorMessage';
import { openInNewTab } from 'utils/navigation';
import { popupContainer } from 'utils/selectPopupContainer';
import ChannelSelect from './ChannelSelect';
@@ -87,7 +88,7 @@ function BasicInfo({
dataSource: ALERTS_DATA_SOURCE_MAP[alertDef?.alertType as AlertTypes],
ruleId: isNewRule ? 0 : alertDef?.id,
});
window.open(ROUTES.CHANNELS_NEW, '_blank');
openInNewTab(ROUTES.CHANNELS_NEW);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasLoggedEvent = useRef(false);

View File

@@ -46,6 +46,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { isModifierKeyPressed } from 'utils/app';
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
import { openInNewTab } from 'utils/navigation';
import BasicInfo from './BasicInfo';
import ChartPreview from './ChartPreview';
@@ -771,7 +772,7 @@ function FormAlertRules({
queryType: currentQuery.queryType,
link: url,
});
window.open(url, '_blank');
openInNewTab(url);
}
}

View File

@@ -10,6 +10,7 @@ import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { Dashboard } from 'types/api/dashboard/getAll';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import dialsUrl from '@/assets/Icons/dials.svg';
@@ -114,7 +115,7 @@ export default function Dashboards({
dashboardName: dashboard.data.title,
});
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
openInNewTab(getLink());
} else {
safeNavigate(getLink());
}

View File

@@ -9,6 +9,7 @@ import { Link2 } from 'lucide-react';
import Card from 'periscope/components/Card/Card';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { openInNewTab } from 'utils/navigation';
import containerPlusUrl from '@/assets/Icons/container-plus.svg';
import helloWaveUrl from '@/assets/Icons/hello-wave.svg';
@@ -51,7 +52,7 @@ function DataSourceInfo({
if (activeLicense && activeLicense.platform === LicensePlatform.CLOUD) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(DOCS_LINKS.ADD_DATA_SOURCE, '_blank', 'noopener noreferrer');
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
}
};

View File

@@ -8,6 +8,7 @@ import { ArrowRight, ArrowRightToLine, BookOpenText } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import './HomeChecklist.styles.scss';
@@ -99,11 +100,7 @@ function HomeChecklist({
) {
history.push(item.toRoute || '');
} else {
window?.open(
item.docsLink || '',
'_blank',
'noopener noreferrer',
);
openInNewTab(item.docsLink || '');
}
}}
>
@@ -119,7 +116,7 @@ function HomeChecklist({
step: item.id,
});
window?.open(item.docsLink, '_blank', 'noopener noreferrer');
openInNewTab(item.docsLink ?? '');
}}
>
<BookOpenText size={16} />

View File

@@ -31,6 +31,7 @@ import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -79,11 +80,7 @@ const EmptyState = memo(
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
}
}}
>

View File

@@ -17,6 +17,7 @@ import { ServicesList } from 'types/api/metrics/getService';
import { GlobalReducer } from 'types/reducer/globalTime';
import { USER_ROLES } from 'types/roles';
import { isModifierKeyPressed } from 'utils/app';
import { openInNewTab } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
@@ -133,11 +134,7 @@ export default function ServiceTraces({
) {
history.push(ROUTES.GET_STARTED_WITH_CLOUD);
} else {
window?.open(
DOCS_LINKS.ADD_DATA_SOURCE,
'_blank',
'noopener noreferrer',
);
openInNewTab(DOCS_LINKS.ADD_DATA_SOURCE);
}
}}
>

View File

@@ -49,6 +49,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import ClusterEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -414,10 +415,7 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -436,10 +434,7 @@ function ClusterDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -48,6 +48,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DaemonSetEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -429,10 +430,7 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -451,10 +449,7 @@ function DaemonSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import DeploymentEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -433,10 +434,7 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -455,10 +453,7 @@ function DeploymentDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -48,6 +48,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import JobEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -427,10 +428,7 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -449,10 +447,7 @@ function JobDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NamespaceEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -419,10 +420,7 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -441,10 +439,7 @@ function NamespaceDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import NodeLogs from '../../EntityDetailsUtils/EntityLogs';
@@ -416,10 +417,7 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -438,10 +436,7 @@ function NodeDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -50,6 +50,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import PodEvents from '../../EntityDetailsUtils/EntityEvents';
@@ -435,10 +436,7 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -457,10 +455,7 @@ function PodDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -53,6 +53,7 @@ import {
TracesAggregatorOperator,
} from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuidv4 } from 'uuid';
import {
@@ -431,10 +432,7 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
} else if (selectedView === VIEW_TYPES.TRACES) {
const compositeQuery = {
...initialQueryState,
@@ -453,10 +451,7 @@ function StatefulSetDetails({
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
window.open(
`${window.location.origin}${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`,
'_blank',
);
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
}
};

View File

@@ -1,5 +1,6 @@
import { ArrowRightOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import { openInNewTab } from 'utils/navigation';
interface AlertInfoCardProps {
header: string;
@@ -19,7 +20,7 @@ function AlertInfoCard({
className="alert-info-card"
onClick={(): void => {
onClick();
window.open(link, '_blank');
openInNewTab(link);
}}
>
<div className="alert-card-text">

View File

@@ -1,5 +1,6 @@
import { ArrowRightOutlined, PlayCircleFilled } from '@ant-design/icons';
import { Flex, Typography } from 'antd';
import { openInNewTab } from 'utils/navigation';
interface InfoLinkTextProps {
infoText: string;
@@ -20,7 +21,7 @@ function InfoLinkText({
<Flex
onClick={(): void => {
onClick();
window.open(link, '_blank');
openInNewTab(link);
}}
className="info-link-container"
>

View File

@@ -83,6 +83,8 @@ import {
} from 'types/api/dashboard/getAll';
import APIError from 'types/api/error';
import { isModifierKeyPressed } from 'utils/app';
import { getAbsoluteUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
import dashboardsUrl from '@/assets/Icons/dashboards.svg';
@@ -457,7 +459,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
window.open(getLink(), '_blank');
openInNewTab(getLink());
}}
>
Open in New Tab
@@ -469,7 +471,7 @@ function DashboardsList(): JSX.Element {
onClick={(e): void => {
e.stopPropagation();
e.preventDefault();
setCopy(`${window.location.origin}${getLink()}`);
setCopy(getAbsoluteUrl(getLink()));
}}
>
Copy Link

View File

@@ -1,6 +1,7 @@
import { LockFilled } from '@ant-design/icons';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { openInNewTab } from 'utils/navigation';
import { Data } from '../DashboardsList';
import { TableLinkText } from './styles';
@@ -12,7 +13,7 @@ function Name(name: Data['name'], data: Data): JSX.Element {
const onClickHandler = (event: React.MouseEvent<HTMLElement>): void => {
if (event.metaKey || event.ctrlKey) {
window.open(getLink(), '_blank');
openInNewTab(getLink());
} else {
history.push(getLink());
}

View File

@@ -17,6 +17,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import { ILog } from 'types/api/logs/log';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import { withBasePath } from 'utils/basePath';
import { useContextLogData } from './useContextLogData';
@@ -116,7 +117,7 @@ function ContextLogRenderer({
);
const link = `${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`;
window.open(link, '_blank', 'noopener,noreferrer');
window.open(withBasePath(link), '_blank', 'noopener,noreferrer');
},
[query, urlQuery],
);

View File

@@ -34,6 +34,7 @@ import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { openInNewTab } from 'utils/navigation';
import { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer';
@@ -191,7 +192,7 @@ function TableView({
if (event.ctrlKey || event.metaKey) {
// open the trace in new tab
window.open(route, '_blank');
openInNewTab(route);
} else {
history.push(route);
}

View File

@@ -213,6 +213,7 @@ function Login(): JSX.Element {
if (isCallbackAuthN) {
const url = form.getFieldValue('url');
// eslint-disable-next-line rulesdir/no-raw-absolute-path
window.location.href = url;
}
} catch (error) {

View File

@@ -34,6 +34,7 @@ import ROUTES from 'constants/routes';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDragColumns from 'hooks/useDragColumns';
import { getAbsoluteUrl } from 'utils/basePath';
import { infinityDefaultStyles } from '../InfinityTableView/config';
import { TanStackTableStyled } from '../InfinityTableView/styles';
@@ -239,7 +240,7 @@ const TanStackTableView = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
urlQuery.delete(QueryParams.activeLogId);
urlQuery.delete(QueryParams.relativeTime);
urlQuery.set(QueryParams.activeLogId, `"${logId}"`);
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
setCopy(link);
toast.success('Copied to clipboard', { position: 'top-right' });

View File

@@ -1,5 +1,6 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { withBasePath } from 'utils/basePath';
import { TopOperationList } from './TopOperationsTable';
import { NavigateToTraceProps } from './types';
@@ -37,7 +38,7 @@ export const navigateToTrace = ({
}=${JSONCompositeQuery}`;
if (openInNewTab) {
window.open(newTraceExplorerPath, '_blank');
window.open(withBasePath(newTraceExplorerPath), '_blank');
} else {
safeNavigate(newTraceExplorerPath);
}

View File

@@ -9,6 +9,7 @@ import {
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { Bell, Grid } from 'lucide-react';
import { openInNewTab } from 'utils/navigation';
import { pluralize } from 'utils/pluralize';
import { DashboardsAndAlertsPopoverProps } from './types';
@@ -67,9 +68,8 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={alert.alertId}
onClick={(): void => {
window.open(
openInNewTab(
`${ROUTES.ALERT_OVERVIEW}?${QueryParams.ruleId}=${alert.alertId}`,
'_blank',
);
}}
className="dashboards-popover-content-item"
@@ -90,11 +90,10 @@ function DashboardsAndAlertsPopover({
<Typography.Link
key={dashboard.dashboardId}
onClick={(): void => {
window.open(
openInNewTab(
generatePath(ROUTES.DASHBOARD, {
dashboardId: dashboard.dashboardId,
}),
'_blank',
);
}}
className="dashboards-popover-content-item"

View File

@@ -18,6 +18,7 @@ import {
import useContextVariables from 'hooks/dashboard/useContextVariables';
import { Plus, Trash2 } from 'lucide-react';
import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll';
import { getBaseUrl } from 'utils/basePath';
import VariablesDropdown from './VariablesDropdown';
@@ -84,7 +85,7 @@ function UpdateContextLinks({
);
// Function to get current domain
const getCurrentDomain = (): string => window.location.origin;
const getCurrentDomain = (): string => getBaseUrl();
// Function to handle variable selection from dropdown
const handleVariableSelect = (

View File

@@ -6,6 +6,7 @@ import history from 'lib/history';
import { ArrowUpRight } from 'lucide-react';
import { DataSource } from 'types/common/queryBuilder';
import DOCLINKS from 'utils/docLinks';
import { openInNewTab } from 'utils/navigation';
import eyesEmojiUrl from '@/assets/Images/eyesEmoji.svg';
@@ -42,11 +43,11 @@ export default function NoLogs({
}
history.push(link);
} else if (dataSource === 'traces') {
window.open(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE, '_blank');
openInNewTab(DOCLINKS.TRACES_EXPLORER_EMPTY_STATE);
} else if (dataSource === DataSource.METRICS) {
window.open(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE, '_blank');
openInNewTab(DOCLINKS.METRICS_EXPLORER_EMPTY_STATE);
} else {
window.open(`${DOCLINKS.USER_GUIDE}${dataSource}/`, '_blank');
openInNewTab(`${DOCLINKS.USER_GUIDE}${dataSource}/`);
}
};
return (

View File

@@ -18,6 +18,7 @@ import {
Trash2,
} from 'lucide-react';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import { v4 as uuid } from 'uuid';
import { OnboardingQuestionHeader } from '../OnboardingQuestionHeader';
@@ -60,7 +61,7 @@ function InviteTeamMembers({
email: '',
role: '',
name: '',
frontendBaseUrl: window.location.origin,
frontendBaseUrl: getBaseUrl(),
id: '',
};

View File

@@ -8,6 +8,7 @@ import { DOCS_BASE_URL } from 'constants/app';
import { useGetGlobalConfig } from 'hooks/globalConfig/useGetGlobalConfig';
import { useNotifications } from 'hooks/useNotifications';
import { ArrowUpRight, Copy, Info, Key, TriangleAlert } from 'lucide-react';
import { withBasePath } from 'utils/basePath';
import './IngestionDetails.styles.scss';
@@ -215,7 +216,7 @@ export default function OnboardingIngestionDetails(): JSX.Element {
</a>
. To create a new one,{' '}
<a
href="/settings/ingestion-settings"
href={withBasePath('/settings/ingestion-settings')}
target="_blank"
className="learn-more"
rel="noreferrer"

View File

@@ -8,6 +8,7 @@ import { useNotifications } from 'hooks/useNotifications';
import { cloneDeep, debounce, isEmpty } from 'lodash-es';
import { ArrowRight, CheckCircle, Plus, TriangleAlert, X } from 'lucide-react';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import { v4 as uuid } from 'uuid';
import './InviteTeamMembers.styles.scss';
@@ -56,7 +57,7 @@ function InviteTeamMembers({
email: '',
role: 'EDITOR',
name: '',
frontendBaseUrl: window.location.origin,
frontendBaseUrl: getBaseUrl(),
id: '',
};

View File

@@ -18,6 +18,7 @@ import ErrorContent from 'components/ErrorModal/components/ErrorContent';
import CopyToClipboard from 'periscope/components/CopyToClipboard';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { getAbsoluteUrl } from 'utils/basePath';
import CreateEdit from './CreateEdit/CreateEdit';
import SSOEnforcementToggle from './SSOEnforcementToggle';
@@ -145,7 +146,7 @@ function AuthDomain(): JSX.Element {
return <span className="auth-domain-list-na">N/A</span>;
}
const href = `${window.location.origin}/${relayPath}`;
const href = getAbsoluteUrl(`/${relayPath}`);
return <CopyToClipboard textToCopy={href} />;
},
},

View File

@@ -4,6 +4,7 @@ import { Button, Form, FormInstance, Modal } from 'antd';
import sendInvite from 'api/v1/invite/create';
import { useNotifications } from 'hooks/useNotifications';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import InviteTeamMembers from '../InviteTeamMembers';
import { InviteMemberFormValues } from '../utils';
@@ -40,7 +41,7 @@ function InviteUserModal(props: InviteUserModalProps): JSX.Element {
email: member.email,
name: member?.name,
role: member.role,
frontendBaseUrl: window.location.origin,
frontendBaseUrl: getBaseUrl(),
});
notifications.success({

View File

@@ -14,6 +14,7 @@ import ContextMenu from 'periscope/components/ContextMenu';
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { ContextLinksData } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { openInNewTab } from 'utils/navigation';
import { ContextMenuItem } from './contextConfig';
import { getDataLinks } from './dataLinksUtils';
@@ -115,7 +116,7 @@ const useBaseAggregateOptions = ({
key={id}
icon={<LinkOutlined />}
onClick={(): void => {
window.open(url, '_blank');
openInNewTab(url);
onClose?.();
}}
>

View File

@@ -14,6 +14,7 @@ import { ModalTitle } from 'container/PipelinePage/PipelineListsView/styles';
import { Check, Loader, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { USER_ROLES } from 'types/roles';
import { openInNewTab } from 'utils/navigation';
import { INITIAL_ROUTING_POLICY_DETAILS_FORM_STATE } from './constants';
import {
@@ -76,7 +77,7 @@ function RoutingPolicyDetails({
style={{ padding: '0 4px' }}
type="link"
onClick={(): void => {
window.open(ROUTES.CHANNELS_NEW, '_blank');
openInNewTab(ROUTES.CHANNELS_NEW);
}}
>
here.

View File

@@ -818,7 +818,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
);
if (item && !('type' in item) && item.isExternal && item.url) {
window.open(item.url, '_blank');
openInNewTab(item.url);
}
const event = (info as SidebarItem & { domEvent?: MouseEvent }).domEvent;

View File

@@ -28,6 +28,7 @@ import {
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { openInNewTab } from 'utils/navigation';
import { v4 as uuid } from 'uuid';
import noDataUrl from '@/assets/Icons/no-data.svg';
@@ -143,7 +144,7 @@ function SpanLogs({
const url = `${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`;
window.open(url, '_blank');
openInNewTab(url);
},
[
isLogSpanRelated,

View File

@@ -17,6 +17,7 @@ import { BarChart2, Compass, X } from 'lucide-react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Span } from 'types/api/trace/getTraceV2';
import { DataSource, LogsAggregatorOperator } from 'types/common/queryBuilder';
import { getAbsoluteUrl } from 'utils/basePath';
import { RelatedSignalsViews } from '../constants';
import SpanLogs from '../SpanLogs/SpanLogs';
@@ -158,9 +159,7 @@ function SpanRelatedSignals({
searchParams.set(QueryParams.endTime, endTimeMs.toString());
window.open(
`${window.location.origin}${
ROUTES.LOGS_EXPLORER
}?${searchParams.toString()}`,
getAbsoluteUrl(`${ROUTES.LOGS_EXPLORER}?${searchParams.toString()}`),
'_blank',
'noopener,noreferrer',
);

View File

@@ -31,6 +31,7 @@ import {
UPDATE_SPANS_AGGREGATE_PAGE_SIZE,
} from 'types/actions/trace';
import { TraceReducer } from 'types/reducer/trace';
import { openInNewTab } from 'utils/navigation';
import { v4 } from 'uuid';
dayjs.extend(duration);
@@ -214,7 +215,7 @@ function TraceTable(): JSX.Element {
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getLink(record), '_blank');
openInNewTab(getLink(record));
} else {
history.push(getLink(record));
}

View File

@@ -28,6 +28,7 @@ import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { openInNewTab } from 'utils/navigation';
import './TracesTableComponent.styles.scss';
@@ -86,7 +87,7 @@ function TracesTableComponent({
event.preventDefault();
event.stopPropagation();
if (event.metaKey || event.ctrlKey) {
window.open(getTraceLink(record), '_blank');
openInNewTab(getTraceLink(record));
} else {
history.push(getTraceLink(record));
}

View File

@@ -17,6 +17,7 @@ import {
ConnectionUrlResponse,
GenerateConnectionUrlPayload,
} from 'types/api/integrations/aws';
import { openInNewTab } from 'utils/navigation';
import { regions } from 'utils/regions';
import logEvent from '../../../api/common/logEvent';
@@ -120,7 +121,7 @@ export function useIntegrationModal({
logEvent('AWS Integration: Account connection attempt redirected to AWS', {
id: data.account_id,
});
window.open(data.connection_url, '_blank');
openInNewTab(data.connection_url);
setModalState(ModalStateEnum.WAITING);
setAccountId(data.account_id);
},

View File

@@ -17,6 +17,7 @@ import useUrlQuery from 'hooks/useUrlQuery';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getAbsoluteUrl } from 'utils/basePath';
import { HIGHLIGHTED_DELAY } from './configs';
import { UseCopyLogLink } from './types';
@@ -60,7 +61,7 @@ export const useCopyLogLink = (logId?: string): UseCopyLogLink => {
urlQuery.set(QueryParams.startTime, minTime?.toString() || '');
urlQuery.set(QueryParams.endTime, maxTime?.toString() || '');
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
setCopy(link);

View File

@@ -21,6 +21,7 @@ import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
import { AppState } from 'store/reducers';
import { Widgets } from 'types/api/dashboard/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { withBasePath } from 'utils/basePath';
import { getGraphType } from 'utils/getGraphType';
const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
@@ -92,7 +93,7 @@ const useCreateAlerts = (widget?: Widgets, caller?: string): VoidFunction => {
const url = `${ROUTES.ALERTS_NEW}?${params.toString()}`;
window.open(url, '_blank', 'noreferrer');
window.open(withBasePath(url), '_blank', 'noreferrer');
},
onError: () => {
notifications.error({

View File

@@ -4,6 +4,7 @@ import { useCopyToClipboard } from 'react-use';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { Span } from 'types/api/trace/getTraceV2';
import { getAbsoluteUrl } from 'utils/basePath';
export const useCopySpanLink = (
span?: Span,
@@ -28,7 +29,7 @@ export const useCopySpanLink = (
urlQuery.set('spanId', span?.spanId);
}
const link = `${window.location.origin}${pathname}?${urlQuery.toString()}`;
const link = getAbsoluteUrl(`${pathname}?${urlQuery.toString()}`);
setCopy(link);
notifications.success({

View File

@@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { cloneDeep, isEqual } from 'lodash-es';
import { withBasePath } from 'utils/basePath';
interface NavigateOptions {
replace?: boolean;
@@ -107,19 +108,18 @@ export const useSafeNavigate = (
const safeNavigate = useCallback(
// eslint-disable-next-line sonarjs/cognitive-complexity
(to: string | SafeNavigateParams, options?: NavigateOptions) => {
const currentUrl = new URL(
`${location.pathname}${location.search}`,
window.location.origin,
);
// eslint-disable-next-line rulesdir/no-raw-absolute-path
const base = window.location.origin;
const currentUrl = new URL(`${location.pathname}${location.search}`, base);
let targetUrl: URL;
if (typeof to === 'string') {
targetUrl = new URL(to, window.location.origin);
targetUrl = new URL(to, base);
} else {
targetUrl = new URL(
`${to.pathname || location.pathname}${to.search || ''}`,
window.location.origin,
base,
);
}
@@ -130,7 +130,7 @@ export const useSafeNavigate = (
typeof to === 'string'
? to
: `${to.pathname || location.pathname}${to.search || ''}`;
window.open(targetPath, '_blank');
window.open(withBasePath(targetPath), '_blank');
return;
}

View File

@@ -1,3 +1,4 @@
import { createBrowserHistory } from 'history';
import { getBasePath } from 'utils/basePath';
export default createBrowserHistory();
export default createBrowserHistory({ basename: getBasePath() });

View File

@@ -4,6 +4,7 @@ import ROUTES from 'constants/routes';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
import { Home, LifeBuoy } from 'lucide-react';
import { handleContactSupport } from 'pages/Integrations/utils';
import { withBasePath } from 'utils/basePath';
import cloudUrl from '@/assets/Images/cloud.svg';
@@ -11,8 +12,8 @@ import './ErrorBoundaryFallback.styles.scss';
function ErrorBoundaryFallback(): JSX.Element {
const handleReload = (): void => {
// Go to home page
window.location.href = ROUTES.HOME;
// Hard reload resets Sentry.ErrorBoundary state; withBasePath preserves any /signoz/ prefix.
window.location.href = withBasePath(ROUTES.HOME);
};
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();

View File

@@ -19,6 +19,7 @@ import {
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { openInNewTab } from 'utils/navigation';
import {
convertToMilliseconds,
@@ -93,7 +94,7 @@ export function getColumns(
key={item}
className="traceid-text"
onClick={(): void => {
window.open(`${ROUTES.TRACE}/${item}`, '_blank');
openInNewTab(`${ROUTES.TRACE}/${item}`);
logEvent(`MQ Kafka: Drop Rate - traceid navigation`, {
item,
});
@@ -123,7 +124,7 @@ export function getColumns(
onClick={(e): void => {
e.preventDefault();
e.stopPropagation();
window.open(`/services/${encodeURIComponent(text)}`, '_blank');
openInNewTab(`/services/${encodeURIComponent(text)}`);
}}
>
{text}

View File

@@ -59,7 +59,7 @@ function MessagingQueues(): JSX.Element {
history.push(link);
}
} else {
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
openInNewTab(KAFKA_SETUP_DOC_LINK);
}
};

View File

@@ -20,6 +20,8 @@ import { useAppContext } from 'providers/App/App';
import { SuccessResponseV2 } from 'types/api';
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
import APIError from 'types/api/error';
import { getBaseUrl } from 'utils/basePath';
import { openInNewTab } from 'utils/navigation';
import './Support.styles.scss';
@@ -92,7 +94,7 @@ export default function Support(): JSX.Element {
const { pathname } = useLocation();
const handleChannelWithRedirects = (url: string): void => {
window.open(url, '_blank');
openInNewTab(url);
};
useEffect(() => {
@@ -150,7 +152,7 @@ export default function Support(): JSX.Element {
});
updateCreditCard({
url: window.location.origin,
url: getBaseUrl(),
});
};

View File

@@ -29,6 +29,7 @@ import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { LicensePlatform } from 'types/api/licensesV3/getActive';
import { isModifierKeyPressed } from 'utils/app';
import { getBaseUrl } from 'utils/basePath';
import { getFormattedDate } from 'utils/timeUtils';
import CustomerStoryCard from './CustomerStoryCard';
@@ -115,7 +116,7 @@ export default function WorkspaceBlocked(): JSX.Element {
logEvent('Workspace Blocked: User Clicked Update Credit Card', {});
updateCreditCard({
url: window.location.origin,
url: getBaseUrl(),
});
}, [updateCreditCard]);

View File

@@ -21,6 +21,7 @@ import history from 'lib/history';
import { useAppContext } from 'providers/App/App';
import APIError from 'types/api/error';
import { LicensePlatform, LicenseState } from 'types/api/licensesV3/getActive';
import { getBaseUrl } from 'utils/basePath';
import { getFormattedDateWithMinutes } from 'utils/timeUtils';
import featureGraphicCorrelationUrl from '@/assets/Images/feature-graphic-correlation.svg';
@@ -57,7 +58,7 @@ function WorkspaceSuspended(): JSX.Element {
const handleUpdateCreditCard = useCallback(async () => {
manageCreditCard({
url: window.location.origin,
url: getBaseUrl(),
});
}, [manageCreditCard]);

View File

@@ -22,6 +22,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
import { EventListener, EventSourcePolyfill } from 'event-source-polyfill';
import { useNotifications } from 'hooks/useNotifications';
import APIError from 'types/api/error';
import { withBasePath } from 'utils/basePath';
interface IEventSourceContext {
eventSourceInstance: EventSourcePolyfill | null;
@@ -129,9 +130,12 @@ export function EventSourceProvider({
const handleStartOpenConnection = useCallback(
(filterExpression?: string): void => {
const eventSourceUrl = `${
ENVIRONMENT.baseURL
}${apiV3}logs/livetail?filter=${encodeURIComponent(filterExpression || '')}`;
const apiPath = `${apiV3}logs/livetail?filter=${encodeURIComponent(
filterExpression || '',
)}`;
const eventSourceUrl = ENVIRONMENT.baseURL
? `${ENVIRONMENT.baseURL}${apiPath}`
: withBasePath(apiPath);
eventSourceRef.current = new EventSourcePolyfill(eventSourceUrl, {
headers: {

View File

@@ -0,0 +1,118 @@
/**
* basePath is memoized at module init, so each describe block isolates the
* module with a fresh DOM state using jest.isolateModules + require.
*/
type BasePath = typeof import('../basePath');
function loadModule(href?: string): BasePath {
if (href !== undefined) {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.appendChild(base);
}
let mod!: BasePath;
jest.isolateModules(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
mod = require('../basePath');
});
return mod;
}
afterEach(() => {
document.head.querySelectorAll('base').forEach((el) => el.remove());
});
describe('at basePath="/"', () => {
let m: BasePath;
beforeEach(() => {
m = loadModule('/');
});
it('getBasePath returns "/"', () => {
expect(m.getBasePath()).toBe('/');
});
it('withBasePath is a no-op for any internal path', () => {
expect(m.withBasePath('/logs')).toBe('/logs');
expect(m.withBasePath('/logs/explorer')).toBe('/logs/explorer');
});
it('withBasePath passes through external URLs', () => {
expect(m.withBasePath('https://example.com/foo')).toBe(
'https://example.com/foo',
);
});
it('getAbsoluteUrl returns origin + path', () => {
expect(m.getAbsoluteUrl('/logs')).toBe(`${window.location.origin}/logs`);
});
it('getBaseUrl returns bare origin', () => {
expect(m.getBaseUrl()).toBe(window.location.origin);
});
});
describe('at basePath="/signoz/"', () => {
let m: BasePath;
beforeEach(() => {
m = loadModule('/signoz/');
});
it('getBasePath returns "/signoz/"', () => {
expect(m.getBasePath()).toBe('/signoz/');
});
it('withBasePath prepends the prefix', () => {
expect(m.withBasePath('/logs')).toBe('/signoz/logs');
expect(m.withBasePath('/logs/explorer')).toBe('/signoz/logs/explorer');
});
it('withBasePath is idempotent — safe to call twice', () => {
expect(m.withBasePath('/signoz/logs')).toBe('/signoz/logs');
});
it('withBasePath is idempotent when path equals the prefix without trailing slash', () => {
expect(m.withBasePath('/signoz')).toBe('/signoz');
});
it('withBasePath passes through external URLs', () => {
expect(m.withBasePath('https://example.com/foo')).toBe(
'https://example.com/foo',
);
});
it('getAbsoluteUrl returns origin + prefixed path', () => {
expect(m.getAbsoluteUrl('/logs')).toBe(
`${window.location.origin}/signoz/logs`,
);
});
it('getBaseUrl returns origin + prefix without trailing slash', () => {
expect(m.getBaseUrl()).toBe(`${window.location.origin}/signoz`);
});
});
describe('no <base> tag', () => {
it('getBasePath falls back to "/"', () => {
const m = loadModule();
expect(m.getBasePath()).toBe('/');
});
});
describe('href without trailing slash', () => {
it('normalises to trailing slash', () => {
const m = loadModule('/signoz');
expect(m.getBasePath()).toBe('/signoz/');
expect(m.withBasePath('/logs')).toBe('/signoz/logs');
});
});
describe('nested prefix "/a/b/prefix/"', () => {
it('withBasePath handles arbitrary depth', () => {
const m = loadModule('/a/b/prefix/');
expect(m.withBasePath('/logs')).toBe('/a/b/prefix/logs');
expect(m.withBasePath('/a/b/prefix/logs')).toBe('/a/b/prefix/logs');
});
});

View File

@@ -1,15 +1,27 @@
import { isModifierKeyPressed } from '../app';
import { openInNewTab } from '../navigation';
type NavigationModule = typeof import('../navigation');
function loadNavigationModule(href?: string): NavigationModule {
if (href !== undefined) {
const base = document.createElement('base');
base.setAttribute('href', href);
document.head.appendChild(base);
}
let mod!: NavigationModule;
jest.isolateModules(() => {
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
mod = require('../navigation');
});
return mod;
}
describe('navigation utilities', () => {
const originalWindowOpen = window.open;
beforeEach(() => {
window.open = jest.fn();
});
afterEach(() => {
window.open = originalWindowOpen;
document.head.querySelectorAll('base').forEach((el) => el.remove());
});
describe('isModifierKeyPressed', () => {
@@ -56,25 +68,59 @@ describe('navigation utilities', () => {
});
describe('openInNewTab', () => {
it('calls window.open with the given path and _blank target', () => {
openInNewTab('/dashboard');
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
describe('at basePath="/"', () => {
let m: NavigationModule;
beforeEach(() => {
window.open = jest.fn();
m = loadNavigationModule('/');
});
it('passes internal path through unchanged', () => {
m.openInNewTab('/dashboard');
expect(window.open).toHaveBeenCalledWith('/dashboard', '_blank');
});
it('passes through external URLs unchanged', () => {
m.openInNewTab('https://example.com/page');
expect(window.open).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
it('handles paths with query strings', () => {
m.openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
expect(window.open).toHaveBeenCalledWith(
'/alerts?tab=AlertRules&relativeTime=30m',
'_blank',
);
});
});
it('handles full URLs', () => {
openInNewTab('https://example.com/page');
expect(window.open).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
describe('at basePath="/signoz/"', () => {
let m: NavigationModule;
beforeEach(() => {
window.open = jest.fn();
m = loadNavigationModule('/signoz/');
});
it('handles paths with query strings', () => {
openInNewTab('/alerts?tab=AlertRules&relativeTime=30m');
expect(window.open).toHaveBeenCalledWith(
'/alerts?tab=AlertRules&relativeTime=30m',
'_blank',
);
it('prepends base path to internal paths', () => {
m.openInNewTab('/dashboard');
expect(window.open).toHaveBeenCalledWith('/signoz/dashboard', '_blank');
});
it('passes through external URLs unchanged', () => {
m.openInNewTab('https://example.com/page');
expect(window.open).toHaveBeenCalledWith(
'https://example.com/page',
'_blank',
);
});
it('is idempotent — does not double-prefix an already-prefixed path', () => {
m.openInNewTab('/signoz/dashboard');
expect(window.open).toHaveBeenCalledWith('/signoz/dashboard', '_blank');
});
});
});
});

View File

@@ -0,0 +1,51 @@
// Read once at module init — avoids a DOM query on every axios request.
const _basePath: string = ((): string => {
const href = document.querySelector('base')?.getAttribute('href') ?? '/';
return href.endsWith('/') ? href : `${href}/`;
})();
/** Returns the runtime base path — always trailing-slashed. e.g. "/" or "/signoz/" */
export function getBasePath(): string {
return _basePath;
}
/**
* Prepends the base path to an internal absolute path.
* Idempotent and safe to call on any value.
*
* withBasePath('/logs') → '/signoz/logs'
* withBasePath('/signoz/logs') → '/signoz/logs' (already prefixed)
* withBasePath('https://x.com') → 'https://x.com' (external, passthrough)
*/
export function withBasePath(path: string): string {
if (!path.startsWith('/')) {
return path;
}
if (_basePath === '/') {
return path;
}
if (path.startsWith(_basePath) || path === _basePath.slice(0, -1)) {
return path;
}
return _basePath + path.slice(1);
}
/**
* Full absolute URL — for copy-to-clipboard and window.open calls.
* getAbsoluteUrl(ROUTES.LOGS_EXPLORER) → 'https://host/signoz/logs/logs-explorer'
*/
export function getAbsoluteUrl(path: string): string {
// eslint-disable-next-line rulesdir/no-raw-absolute-path
return window.location.origin + withBasePath(path);
}
/**
* Origin + base path without trailing slash — for sending to the backend
* as frontendBaseUrl in invite / password-reset email flows.
* getBaseUrl() → 'https://host/signoz'
*/
export function getBaseUrl(): string {
// eslint-disable-next-line rulesdir/no-raw-absolute-path
const origin = window.location.origin;
return origin + (_basePath === '/' ? '' : _basePath.slice(0, -1));
}

View File

@@ -1,6 +1,5 @@
/**
* Opens the given path in a new browser tab.
*/
import { withBasePath } from 'utils/basePath';
export const openInNewTab = (path: string): void => {
window.open(path, '_blank');
window.open(withBasePath(path), '_blank');
};

View File

@@ -10,6 +10,18 @@ import { createHtmlPlugin } from 'vite-plugin-html';
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
import tsconfigPaths from 'vite-tsconfig-paths';
// In dev the Go backend is not involved, so replace the [[.BaseHref]] placeholder
// with "/" so relative assets resolve correctly from the Vite dev server.
function devBasePathPlugin(): Plugin {
return {
name: 'dev-base-path',
apply: 'serve',
transformIndexHtml(html): string {
return html.replaceAll('[[.BaseHref]]', '/');
},
};
}
function rawMarkdownPlugin(): Plugin {
return {
name: 'raw-markdown',
@@ -32,6 +44,7 @@ export default defineConfig(
const plugins = [
tsconfigPaths(),
rawMarkdownPlugin(),
devBasePathPlugin(),
react(),
createHtmlPlugin({
inject: {
@@ -124,6 +137,7 @@ export default defineConfig(
'process.env.TUNNEL_DOMAIN': JSON.stringify(env.VITE_TUNNEL_DOMAIN),
'process.env.DOCS_BASE_URL': JSON.stringify(env.VITE_DOCS_BASE_URL),
},
base: './',
build: {
sourcemap: true,
outDir: 'build',

View File

@@ -5503,20 +5503,21 @@
resolved "https://registry.yarnpkg.com/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz#a90ab31d0cc1dfb54c66a69e515bf624fa7b2224"
integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
"@signozhq/button@0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.1.tgz#7d3204454b0361bd3fdf91fa6604af01a481a9db"
integrity sha512-k5WFpckNXzwcTS82jU+65M3V1KdriopBObB1ls7W2OU0RKof6Gf+/9uqDXnuu+Y4Cxn2cPo8+6MfiQbS02LHeg==
"@signozhq/button@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.5.tgz#e8220a6e9ed78552694f41700c277956f26232e1"
integrity sha512-fgobypuXv2kWGDkqXZoEjcySPHELzI/X515cdcR1hx4N9rizzOglLtYEjGTLR13iQrzLwSsNX8xxsv0/iSCjQg==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
"@signozhq/icons" "^0.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/button@0.0.2", "@signozhq/button@^0.0.2":
"@signozhq/button@^0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.2.tgz#c13edef1e735134b784a41f874b60a14bc16993f"
integrity sha512-434/gbTykC00LrnzFPp7c33QPWZkf9n+8+SToLZFTB0rzcaS/xoB4b7QKhvk+8xLCj4zpw6BxfeRAL+gSoOUJw==
@@ -5529,14 +5530,28 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/calendar@0.0.0":
version "0.0.0"
resolved "https://registry.yarnpkg.com/@signozhq/calendar/-/calendar-0.0.0.tgz#93b2cec2586efee814df934f88a2193cec95bae9"
integrity sha512-lm7tzPEhaHNjrksvi2GPGH4suEe6x2DQJ2dpku+JmKyLGB5rg9saSAosvrZVKhXLoZuSSjlBSkz+oHYEKIdHfA==
"@signozhq/button@workspace:*":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.3.tgz#ce6c722b24859198f8c18a708b9d09249b184e3e"
integrity sha512-b0JGoP0AIoYep/ApQUOn9LiK8dUATfqikz7jAKe20dPc8SjwUtsLPOao9j6I+2W4qd0CZDffJADBVpF2SAIsPQ==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
"@signozhq/icons" "^0.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/calendar@0.1.1":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@signozhq/calendar/-/calendar-0.1.1.tgz#8fb6432c4397ad2e8204b0ce9fb1b7aaa231e4cc"
integrity sha512-Mw5cVtqSI1F57YwG+ufuJKJs/KrkrTSeP6aVPcTvO3tHJ5H4aZ1oaeZtGzTEDMqUvusDlRdZPSHhklJiUg/ixQ==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.2.3"
"@signozhq/button" "0.0.1"
"@signozhq/button" "workspace:*"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
date-fns "^4.1.0"
@@ -5545,13 +5560,14 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/callout@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/callout/-/callout-0.0.2.tgz#131ca15f89a8ee6729fecc4d322f11359c02e5cf"
integrity sha512-tmguHm+/JVRKjMElJOFyG7LJcdqCW1hHnFfp8ZkjQ+Gi7MfFt/r2foLZG2DNdOcfxSvhf2zhzr7D+epgvmbQ1A==
"@signozhq/callout@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@signozhq/callout/-/callout-0.0.4.tgz#8d0c224cc4f64930a04bd0d075597358ae7ec1d8"
integrity sha512-g1NXkzAkuMdmH+z58t9CSL4+MZdWB5zPLyOHKeJYnQk1JXSYpEGK8CzSz4ZtVb4OrMS7mDkWxnqMK0EHxwVOWA==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
"@signozhq/icons" "^0.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
@@ -5559,10 +5575,10 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/checkbox@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/checkbox/-/checkbox-0.0.2.tgz#d11fb5eff3927c540937e3bd24351bfc1fdef9ec"
integrity sha512-odQdh839GaTy1kqC8yavUKrOYP5tiIppUIV7xGNyxs/KnLGDWLw3ZSdACRV1Z55CLddjQ6OWKiwyVV7t+sxEuw==
"@signozhq/checkbox@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@signozhq/checkbox/-/checkbox-0.0.4.tgz#2cf83d7bfd4db4aaec815e5788061e3fe5f86483"
integrity sha512-X93EqHEy06pfpGcEJkJpNrxvkZryJaA+M0NQ09oTb1wzFweGMw+fR1Hgjl8lMqQPkHtz3MCP820uLhxvOJhktg==
dependencies:
"@radix-ui/react-checkbox" "^1.2.3"
"@radix-ui/react-icons" "^1.3.0"
@@ -5573,10 +5589,24 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/combobox@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/combobox/-/combobox-0.0.2.tgz#019cc6d619e4eb6d1061fdfa00d4bd99d6aa727f"
integrity sha512-QnGCNJAHd55Wqblw0CLOEOJoLFx8dgP+q/9hXbN5qil72DjRzxBgb5DAkIkon0owCmlagDQknFiOygYnzVJS8g==
"@signozhq/checkbox@workspace:*":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@signozhq/checkbox/-/checkbox-0.0.3.tgz#9ccf8bd3b118405b2407c71db780089de050e651"
integrity sha512-x2uqV3GsLmnDz4Zd2oY9/sExSqTY9gw/tw5gVd9ZziSNWOhy9rBKI8xwVpaAYiaZZHH3NReEwASwjx7C4EKKiQ==
dependencies:
"@radix-ui/react-checkbox" "^1.2.3"
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/combobox@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@signozhq/combobox/-/combobox-0.0.4.tgz#7195416144ae881874e3744f7b71251fbacc4f3e"
integrity sha512-8mXlpkZ6066+JE+9EXmuR47ky+tJLwZ1W/9qXa69x5F1r7zXxuWvNQe/GYiGetpxohV/jO6QQDN1WpCV9hNjiw==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-popover" "^1.1.2"
@@ -5589,10 +5619,10 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/command@0.0.0":
version "0.0.0"
resolved "https://registry.yarnpkg.com/@signozhq/command/-/command-0.0.0.tgz#bd1e1cac7346e862dd61df64b756302e89e1a322"
integrity sha512-AwRYxZTi4o8SBOL4hmgcgbhCKXl2Qb/TUSLbSYEMFdiQSl5VYA8XZJv5fSYVMJkAIlOaHzFzR04XNEU7lZcBpw==
"@signozhq/command@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/command/-/command-0.0.2.tgz#9d72f0d8d0945773461350f8d311072b2b4f96c6"
integrity sha512-MFMDtm6qC/aG84k+q9XVCkRR46kNQYVP0qP59Xg34gx5baD76iivu13rZLc27MEhOucHacP2UODRJ4uMGZt/Mw==
dependencies:
"@radix-ui/react-dialog" "^1.1.11"
"@radix-ui/react-icons" "^1.3.0"
@@ -5609,24 +5639,25 @@
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-2.1.4.tgz#f209da6fbd2ac97ab4434b71472f741009306550"
integrity sha512-Ny7/VA5YGFFmZx58jMh7ATFyu7VePaJ4ySmj/DopP1hilmfdxQsKWnpqKaZJWRXrbNkc0gmq3cR7q7Z8nnN7ZQ==
"@signozhq/dialog@^0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/dialog/-/dialog-0.0.2.tgz#55bd8e693f76325fda9aabe3197350e0adc163c4"
integrity sha512-YT5t3oZpGkAuWptTqhCgVtLjxsRQrEIrQHFoXpP9elM1+O4TS9WHr+07BLQutOVg6u9n9pCvW3OYf0SCETkDVQ==
"@signozhq/dialog@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@signozhq/dialog/-/dialog-0.0.4.tgz#54f385aa7cc17c6281653437e448f81dee0190ff"
integrity sha512-j/RPhx98sCTyfg1VlSxbHfLzG/cVKCdd1JZrVkfzs4WK1ijM7Scpl4MUnFxn0ShCFrJ8trbR6I5NioKUIojK2g==
dependencies:
"@radix-ui/react-dialog" "^1.1.11"
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
"@signozhq/checkbox" "workspace:*"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/drawer@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@signozhq/drawer/-/drawer-0.0.4.tgz#7c6e6779602113f55df8a55076e68b9cc13c7d79"
integrity sha512-m/shStl5yVPjHjrhDAh3EeKqqTtMmZUBVlgJPUGgoNV3sFsuN6JNaaAtEJI8cQBWkbEEiHLWKVkL/vhbQ7YrUg==
"@signozhq/drawer@0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@signozhq/drawer/-/drawer-0.0.6.tgz#ea280d71af6bc665679c7da26e0703a7bf96aa0e"
integrity sha512-xoDHdsVZuj4rHK5gTnM4px7E2rv/6Jgqm81uxs1CfheOQsEAnPuiq3Dpdrdsf6XxCeORwnpcGUR5PEJhnAsjmA==
dependencies:
"@radix-ui/react-dialog" "^1.1.11"
"@radix-ui/react-icons" "^1.3.0"
@@ -5647,23 +5678,24 @@
"@commitlint/cli" "^17.6.7"
"@commitlint/config-conventional" "^17.6.7"
"@signozhq/input@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/input/-/input-0.0.2.tgz#b2fea8c0979a53984ebcd5e3c3c50b38082eb1b1"
integrity sha512-Iti9GkvexSsULX1pQsN6FT6Gw96YWilts72wITZd5fzgZq1yKqaDtQl98/QNuyoS3I3WEh+hVF4EIeCCe7oRsQ==
"@signozhq/input@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@signozhq/input/-/input-0.0.4.tgz#f07dddb26ac4dda1cc8e07bfe605e8b34299955e"
integrity sha512-S6tIcrkRZAsmwA80eAJCwZlgHLuioUHMTeszx4PRf/DUqVQ8cjIjmTfHDwzO0zIbMcVHpxqxVNpYArIqUJtgZQ==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
"@signozhq/button" "workspace:*"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/popover@0.0.0":
version "0.0.0"
resolved "https://registry.yarnpkg.com/@signozhq/popover/-/popover-0.0.0.tgz#675baf1c18ca0180369b4df0700c24e2c55ad758"
integrity sha512-XW0MhzxWzZNQWjVeb+BFjiOIbBbYCT+9MCUOIW8kiL0axFaaimnk0QPi1rk09u136MMGByI6fYuCJ5Qa07l1dA==
"@signozhq/popover@0.1.2":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@signozhq/popover/-/popover-0.1.2.tgz#73b418be584e8a671ff4189ed47a540ed73bbde6"
integrity sha512-6TVMVjWuO7XcKfFMrndcmDdg4JsGvRLe0SV43CRPNV6OkL5uFwUHFJZK2BU3v8S9xMoOf3LsdpfxPuCeK8QKCA==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-popover" "^1.1.15"
@@ -5675,10 +5707,10 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/radio-group@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/radio-group/-/radio-group-0.0.2.tgz#4b13567bfee2645226f2cf41f261bbb288e1be4b"
integrity sha512-ahykmA5hPujOC964CFveMlQ12tWSyut2CUiFRqT1QxRkOLS2R44Qn2hh2psqJJ18JMX/24ZYCAIh9Bdd5XW+7g==
"@signozhq/radio-group@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@signozhq/radio-group/-/radio-group-0.0.4.tgz#2fbd28bf48bb761063d642d3a169c3e0c098639d"
integrity sha512-1t1idP+TsYV52GoQM/5KaxUQtA9/CAxAcVOvT6sBXAbyR12azUaR7GL4S0VxRRfOlA+pZ9bT9TqlfQT7LWUTDg==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-radio-group" "^1.3.4"
@@ -5689,10 +5721,10 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/resizable@0.0.0":
version "0.0.0"
resolved "https://registry.yarnpkg.com/@signozhq/resizable/-/resizable-0.0.0.tgz#a517818b9f9bcdaeafc55ae134be86522bc90e9f"
integrity sha512-yAkJdMgTkh8kv42ZuabwTZguxalwYqIp4b44YdSrw6jRUSq9tscUBXVllNN79T71lPUtc5AV13uQ4Qm5AcfVbQ==
"@signozhq/resizable@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/resizable/-/resizable-0.0.2.tgz#76b9212c5f1b1e982013bec1b618e967a752e705"
integrity sha512-xMdCDHOUDssPknFYRmwNTlQIy583wMpJSx6aHmjeYEcVfhDfDBrW+KEko/ODFoM8dOB0CpMtDjlLU14BchsNnA==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
@@ -5721,10 +5753,10 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/toggle-group@0.0.1":
version "0.0.1"
resolved "https://registry.yarnpkg.com/@signozhq/toggle-group/-/toggle-group-0.0.1.tgz#c82ff1da34e77b24da53c2d595ad6b4a0d1b1de4"
integrity sha512-871bQayL5MaqsuNOFHKexidu9W2Hlg1y4xmH8C5mGmlfZ4bd0ovJ9OweQrM6Puys3jeMwi69xmJuesYCfKQc1g==
"@signozhq/toggle-group@0.0.3":
version "0.0.3"
resolved "https://registry.yarnpkg.com/@signozhq/toggle-group/-/toggle-group-0.0.3.tgz#6a38312b46198c555ca4c287d4818135a4a9dabe"
integrity sha512-qWoxC9jxW/otq1AdD/9ATGEJi4KyTfQSY8BSXlqu+4z4iEVjW4eeWSiyaoHcnB9UaypHDMFDMR+gPkyavtMY2Q==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"

View File

@@ -0,0 +1,360 @@
package alertmanagertemplate
import (
"context"
"log/slog"
"sort"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
)
// Templater expands user-authored title and body templates against a group
// of alerts and returns channel-ready strings along with the aggregate data
// a caller might reuse (e.g. to render an email layout around the body).
type Templater interface {
Expand(ctx context.Context, req alertmanagertypes.ExpandRequest, alerts []*types.Alert) (*alertmanagertypes.ExpandResult, error)
}
type templater struct {
tmpl *template.Template
logger *slog.Logger
}
// New returns a Templater bound to the given Prometheus alertmanager
// template and logger.
func New(tmpl *template.Template, logger *slog.Logger) Templater {
return &templater{tmpl: tmpl, logger: logger}
}
func (at *templater) Expand(
ctx context.Context,
req alertmanagertypes.ExpandRequest,
alerts []*types.Alert,
) (*alertmanagertypes.ExpandResult, error) {
ntd := at.buildNotificationTemplateData(ctx, alerts)
missingVars := make(map[string]bool)
title, titleMissingVars, err := at.expandTitle(req.TitleTemplate, ntd)
if err != nil {
return nil, err
}
// if title template results in empty string, use default template
// this happens for rules where custom title annotation was not set
if title == "" && req.DefaultTitleTemplate != "" {
title, err = at.expandDefaultTemplate(ctx, req.DefaultTitleTemplate, alerts)
if err != nil {
return nil, err
}
} else {
mergeMissingVars(missingVars, titleMissingVars)
}
isDefaultBody := false
body, bodyMissingVars, err := at.expandBody(req.BodyTemplate, ntd)
if err != nil {
return nil, err
}
// if body template results in nil, use default template
// this happens for rules where custom body annotation was not set
if body == nil {
isDefaultBody = true
defaultBody, err := at.expandDefaultTemplate(ctx, req.DefaultBodyTemplate, alerts)
if err != nil {
return nil, err
}
body = []string{defaultBody} // default template combines all alerts message into a single body
} else {
mergeMissingVars(missingVars, bodyMissingVars)
}
// convert the internal map to a sorted slice for returning missing variables
missingVarsList := make([]string, 0, len(missingVars))
for k := range missingVars {
missingVarsList = append(missingVarsList, k)
}
sort.Strings(missingVarsList)
return &alertmanagertypes.ExpandResult{
Title: title,
Body: body,
MissingVars: missingVarsList,
IsDefaultBody: isDefaultBody,
NotificationData: ntd,
}, nil
}
// expandDefaultTemplate uses go-template to expand the default template.
func (at *templater) expandDefaultTemplate(
ctx context.Context,
tmplStr string,
alerts []*types.Alert,
) (string, error) {
// if even the default template is empty, return empty string
// this is possible if user added channel with blank template
if tmplStr == "" {
at.logger.WarnContext(ctx, "default template is empty")
return "", nil
}
data := notify.GetTemplateData(ctx, at.tmpl, alerts, at.logger)
result, err := at.tmpl.ExecuteTextString(tmplStr, data)
if err != nil {
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute default template: %s", err.Error())
}
return result, nil
}
// mergeMissingVars adds all keys from src into dst.
func mergeMissingVars(dst, src map[string]bool) {
for k := range src {
dst[k] = true
}
}
// expandTitle expands the title template. Returns empty string if the template is empty.
func (at *templater) expandTitle(
titleTemplate string,
ntd *alertmanagertypes.NotificationTemplateData,
) (string, map[string]bool, error) {
if titleTemplate == "" {
return "", nil, nil
}
processRes, err := preProcessTemplateAndData(titleTemplate, ntd)
if err != nil {
return "", nil, err
}
result, err := at.tmpl.ExecuteTextString(processRes.Template, processRes.Data)
if err != nil {
return "", nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute custom title template: %s", err.Error())
}
return strings.TrimSpace(result), processRes.UnknownVars, nil
}
// expandBody expands the body template for each individual alert. Returns nil if the template is empty.
func (at *templater) expandBody(
bodyTemplate string,
ntd *alertmanagertypes.NotificationTemplateData,
) ([]string, map[string]bool, error) {
if bodyTemplate == "" {
return nil, nil, nil
}
var sb []string
missingVars := make(map[string]bool)
for i := range ntd.Alerts {
processRes, err := preProcessTemplateAndData(bodyTemplate, &ntd.Alerts[i])
if err != nil {
return nil, nil, err
}
part, err := at.tmpl.ExecuteTextString(processRes.Template, processRes.Data)
if err != nil {
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "failed to execute custom body template: %s", err.Error())
}
// add unknown variables and templated text to the result
for k := range processRes.UnknownVars {
missingVars[k] = true
}
if strings.TrimSpace(part) != "" {
sb = append(sb, strings.TrimSpace(part))
}
}
return sb, missingVars, nil
}
// buildNotificationTemplateData creates the NotificationTemplateData using
// info from context and the raw alerts.
func (at *templater) buildNotificationTemplateData(
ctx context.Context,
alerts []*types.Alert,
) *alertmanagertypes.NotificationTemplateData {
// extract the required data from the context
receiver, ok := notify.ReceiverName(ctx)
if !ok {
at.logger.WarnContext(ctx, "missing receiver name in context")
}
groupLabels, ok := notify.GroupLabels(ctx)
if !ok {
at.logger.WarnContext(ctx, "missing group labels in context")
}
// extract the external URL from the template
externalURL := ""
if at.tmpl.ExternalURL != nil {
externalURL = at.tmpl.ExternalURL.String()
}
commonAnnotations := extractCommonKV(alerts, func(a *types.Alert) model.LabelSet { return a.Annotations })
commonLabels := extractCommonKV(alerts, func(a *types.Alert) model.LabelSet { return a.Labels })
// aggregate labels and annotations from all alerts
labels := aggregateKV(alerts, func(a *types.Alert) model.LabelSet { return a.Labels })
annotations := aggregateKV(alerts, func(a *types.Alert) model.LabelSet { return a.Annotations })
// Strip private annotations from surfaces visible to templates or
// notifications; the structured fields on AlertInfo/RuleInfo already hold
// anything a template needs from them.
commonAnnotations = alertmanagertypes.FilterPublicAnnotations(commonAnnotations)
annotations = alertmanagertypes.FilterPublicAnnotations(annotations)
// build the alert data slice
alertDataSlice := make([]alertmanagertypes.AlertData, 0, len(alerts))
for _, a := range alerts {
ad := buildAlertData(a, receiver)
alertDataSlice = append(alertDataSlice, ad)
}
// count the number of firing and resolved alerts
var firing, resolved int
for _, ad := range alertDataSlice {
if ad.Alert.IsFiring {
firing++
} else if ad.Alert.IsResolved {
resolved++
}
}
// build the group labels
gl := make(template.KV, len(groupLabels))
for k, v := range groupLabels {
gl[string(k)] = string(v)
}
// build the notification template data
return &alertmanagertypes.NotificationTemplateData{
Alert: alertmanagertypes.NotificationAlert{
Receiver: receiver,
Status: string(types.Alerts(alerts...).Status()),
TotalFiring: firing,
TotalResolved: resolved,
},
Rule: buildRuleInfo(commonLabels, commonAnnotations),
GroupLabels: gl,
CommonLabels: commonLabels,
CommonAnnotations: commonAnnotations,
ExternalURL: externalURL,
Labels: labels,
Annotations: annotations,
Alerts: alertDataSlice,
}
}
// buildAlertData converts a single *types.Alert into an AlertData.
func buildAlertData(a *types.Alert, receiver string) alertmanagertypes.AlertData {
labels := make(template.KV, len(a.Labels))
for k, v := range a.Labels {
labels[string(k)] = string(v)
}
annotations := make(template.KV, len(a.Annotations))
for k, v := range a.Annotations {
annotations[string(k)] = string(v)
}
return alertmanagertypes.AlertData{
Alert: alertmanagertypes.AlertInfo{
Status: string(a.Status()),
Receiver: receiver,
Value: annotations[ruletypes.AnnotationValue],
StartsAt: a.StartsAt,
EndsAt: a.EndsAt,
GeneratorURL: a.GeneratorURL,
Fingerprint: a.Fingerprint().String(),
IsFiring: a.Status() == model.AlertFiring,
IsResolved: a.Status() == model.AlertResolved,
IsMissingData: labels[ruletypes.LabelNoData] == "true",
IsRecovering: labels[ruletypes.LabelIsRecovering] == "true",
},
Rule: buildRuleInfo(labels, annotations),
Log: alertmanagertypes.LinkInfo{URL: annotations[ruletypes.AnnotationRelatedLogs]},
Trace: alertmanagertypes.LinkInfo{URL: annotations[ruletypes.AnnotationRelatedTraces]},
Labels: labels,
// Strip private annotations once the structured fields above have
// been populated from the raw map.
Annotations: alertmanagertypes.FilterPublicAnnotations(annotations),
}
}
// buildRuleInfo extracts the rule metadata from the well-known labels and
// annotations that the rule manager attaches to every emitted alert.
func buildRuleInfo(labels, annotations template.KV) alertmanagertypes.RuleInfo {
return alertmanagertypes.RuleInfo{
Name: labels[ruletypes.LabelAlertName],
ID: labels[ruletypes.LabelRuleID],
URL: labels[ruletypes.LabelRuleSource],
Severity: labels[ruletypes.LabelSeverityName],
MatchType: annotations[ruletypes.AnnotationMatchType],
Threshold: alertmanagertypes.Threshold{
Value: annotations[ruletypes.AnnotationThresholdValue],
Op: annotations[ruletypes.AnnotationCompareOp],
},
}
}
// maxAggregatedValues caps the number of distinct label/annotation values
// joined together when summarising across alerts. Beyond this, extras are
// dropped rather than concatenated.
const maxAggregatedValues = 5
// aggregateKV merges label or annotation sets from a group of alerts into a
// single KV. Per key, up to maxAggregatedValues distinct values are kept (in
// order of first appearance) and joined with ", ". A lossy summary used for
// grouped-notification display, not a true union.
func aggregateKV(alerts []*types.Alert, extractFn func(*types.Alert) model.LabelSet) template.KV {
valuesPerKey := make(map[string][]string)
seenValues := make(map[string]map[string]bool)
for _, alert := range alerts {
for k, v := range extractFn(alert) {
key := string(k)
value := string(v)
if seenValues[key] == nil {
seenValues[key] = make(map[string]bool)
}
if !seenValues[key][value] && len(valuesPerKey[key]) < maxAggregatedValues {
seenValues[key][value] = true
valuesPerKey[key] = append(valuesPerKey[key], value)
}
}
}
result := make(template.KV, len(valuesPerKey))
for key, values := range valuesPerKey {
result[key] = strings.Join(values, ", ")
}
return result
}
// extractCommonKV returns the intersection of label or annotation pairs
// across all alerts. A pair is included only if every alert carries the same
// key with the same value.
func extractCommonKV(alerts []*types.Alert, extractFn func(*types.Alert) model.LabelSet) template.KV {
if len(alerts) == 0 {
return template.KV{}
}
common := make(template.KV, len(extractFn(alerts[0])))
for k, v := range extractFn(alerts[0]) {
common[string(k)] = string(v)
}
for _, a := range alerts[1:] {
kv := extractFn(a)
for k := range common {
if string(kv[model.LabelName(k)]) != common[k] {
delete(common, k)
}
}
if len(common) == 0 {
break
}
}
return common
}

View File

@@ -0,0 +1,287 @@
package alertmanagertemplate
import (
"context"
"log/slog"
"sort"
"testing"
"time"
test "github.com/SigNoz/signoz/pkg/alertmanager/alertmanagernotify/alertmanagernotifytest"
"github.com/SigNoz/signoz/pkg/types/alertmanagertypes"
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/types"
)
// testSetup returns an AlertTemplater and a context pre-populated with group key,
// receiver name, and group labels for use in tests.
func testSetup(t *testing.T) (Templater, context.Context) {
t.Helper()
tmpl := test.CreateTmpl(t)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "test-group")
ctx = notify.WithReceiverName(ctx, "slack")
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": "TestAlert", "severity": "critical"})
logger := slog.New(slog.DiscardHandler)
return New(tmpl, logger), ctx
}
func createAlert(labels, annotations map[string]string, isFiring bool) *types.Alert {
ls := model.LabelSet{}
for k, v := range labels {
ls[model.LabelName(k)] = model.LabelValue(v)
}
ann := model.LabelSet{}
for k, v := range annotations {
ann[model.LabelName(k)] = model.LabelValue(v)
}
startsAt := time.Now()
var endsAt time.Time
if isFiring {
endsAt = startsAt.Add(time.Hour)
} else {
startsAt = startsAt.Add(-2 * time.Hour)
endsAt = startsAt.Add(-time.Hour)
}
return &types.Alert{Alert: model.Alert{Labels: ls, Annotations: ann, StartsAt: startsAt, EndsAt: endsAt}}
}
func TestExpandTemplates(t *testing.T) {
at, ctx := testSetup(t)
tests := []struct {
name string
alerts []*types.Alert
input alertmanagertypes.ExpandRequest
wantTitle string
wantBody []string
wantMissingVars []string
errorContains string
wantIsDefaultBody bool
}{
{
// High request throughput on a service — service is a custom label.
// $labels.service extracts the label value; $annotations.description pulls the annotation.
name: "new template: high request throughput for a service",
alerts: []*types.Alert{
createAlert(
map[string]string{
ruletypes.LabelAlertName: "HighRequestThroughput",
ruletypes.LabelSeverityName: "warning",
"service.name": "payment-service",
},
map[string]string{"description": "Request rate exceeded 10k/s"},
true,
),
},
input: alertmanagertypes.ExpandRequest{
TitleTemplate: "High request throughput for $service.name",
BodyTemplate: `The service $service.name is getting high request. Please investigate.
Severity: $rule.severity
Status: $alert.status
Service: $service.name
Description: $description`,
},
wantTitle: "High request throughput for payment-service",
wantBody: []string{`The service payment-service is getting high request. Please investigate.
Severity: warning
Status: firing
Service: payment-service
Description: Request rate exceeded 10k/s`},
wantIsDefaultBody: false,
},
{
// Disk usage alert using old Go template syntax throughout.
// No custom templates — both title and body use the default fallback path.
name: "old template: disk usage high on database host",
alerts: []*types.Alert{
createAlert(
map[string]string{ruletypes.LabelAlertName: "DiskUsageHigh",
ruletypes.LabelSeverityName: "critical",
"instance": "db-primary-01",
},
map[string]string{
"summary": "Disk usage high on database host",
"description": "Disk usage is high on the database host",
"related_logs": "https://logs.example.com/search?q=DiskUsageHigh",
"related_traces": "https://traces.example.com/search?q=DiskUsageHigh",
},
true,
),
},
input: alertmanagertypes.ExpandRequest{
DefaultTitleTemplate: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
{{- if gt (len .CommonLabels) (len .GroupLabels) -}}
{{" "}}(
{{- with .CommonLabels.Remove .GroupLabels.Names }}
{{- range $index, $label := .SortedPairs -}}
{{ if $index }}, {{ end }}
{{- $label.Name }}="{{ $label.Value -}}"
{{- end }}
{{- end -}}
)
{{- end }}`,
DefaultBodyTemplate: `{{ range .Alerts -}}
*Alert:* {{ .Labels.alertname }}{{ if .Labels.severity }} - {{ .Labels.severity }}{{ end }}
*Summary:* {{ .Annotations.summary }}
*Description:* {{ .Annotations.description }}
*RelatedLogs:* {{ if gt (len .Annotations.related_logs) 0 -}} View in <{{ .Annotations.related_logs }}|logs explorer> {{- end}}
*RelatedTraces:* {{ if gt (len .Annotations.related_traces) 0 -}} View in <{{ .Annotations.related_traces }}|traces explorer> {{- end}}
*Details:*
{{ range .Labels.SortedPairs }} • *{{ .Name }}:* {{ .Value }}
{{ end }}
{{ end }}`,
},
wantTitle: "[FIRING:1] DiskUsageHigh for (instance=\"db-primary-01\")",
// Written with explicit \n so trailing whitespace inside the body
// (emitted by the un-trimmed "{{ end }}" in the default template)
// survives format-on-save.
wantBody: []string{"*Alert:* DiskUsageHigh - critical\n" +
"\n" +
" *Summary:* Disk usage high on database host\n" +
" *Description:* Disk usage is high on the database host\n" +
" *RelatedLogs:* View in <https://logs.example.com/search?q=DiskUsageHigh|logs explorer>\n" +
" *RelatedTraces:* View in <https://traces.example.com/search?q=DiskUsageHigh|traces explorer>\n" +
"\n" +
" *Details:*\n" +
" • *alertname:* DiskUsageHigh\n" +
" • *instance:* db-primary-01\n" +
" • *severity:* critical\n" +
" \n" +
" "},
wantIsDefaultBody: true,
},
{
// Pod crash loop on multiple pods — body is expanded once per alert
// and joined with "\n\n", with the pod name pulled from labels.
name: "new template: pod crash loop on multiple pods, body per-alert",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-1"}, nil, true),
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-2"}, nil, true),
createAlert(map[string]string{ruletypes.LabelAlertName: "PodCrashLoop", "pod": "api-worker-3"}, nil, true),
},
input: alertmanagertypes.ExpandRequest{
TitleTemplate: "$rule.name: $alert.total_firing pods affected",
BodyTemplate: "$labels.pod is crash looping",
},
wantTitle: "PodCrashLoop: 3 pods affected",
wantBody: []string{"api-worker-1 is crash looping", "api-worker-2 is crash looping", "api-worker-3 is crash looping"},
wantIsDefaultBody: false,
},
{
// Incident partially resolved — one service still down, one recovered.
// Title shows the aggregate counts; body shows per-service status.
name: "new template: service degradation with mixed firing and resolved alerts",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "ServiceDown", "service": "auth-service"}, nil, true),
createAlert(map[string]string{ruletypes.LabelAlertName: "ServiceDown", "service": "payment-service"}, nil, false),
},
input: alertmanagertypes.ExpandRequest{
TitleTemplate: "$alert.total_firing firing, $alert.total_resolved resolved",
BodyTemplate: "$labels.service ($alert.status)",
},
wantTitle: "1 firing, 1 resolved",
wantBody: []string{"auth-service (firing)", "payment-service (resolved)"},
wantIsDefaultBody: false,
},
{
// $environment is not a known AlertData or NotificationTemplateData field,
// so it lands in MissingVars and renders as "<no value>" in the output.
name: "missing vars: unknown $environment variable in title",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, true),
},
input: alertmanagertypes.ExpandRequest{
TitleTemplate: "[$environment] $rule.name",
},
wantTitle: "[<no value>] HighCPU",
wantMissingVars: []string{"environment"},
wantIsDefaultBody: true,
},
{
// $runbook_url is not a known field — someone tried to embed a runbook link
// directly as a variable instead of via annotations.
name: "missing vars: unknown $runbook_url variable in body",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "PodOOMKilled", ruletypes.LabelSeverityName: "warning"}, nil, true),
},
input: alertmanagertypes.ExpandRequest{
BodyTemplate: "$rule.name: see runbook at $runbook_url",
},
wantBody: []string{"PodOOMKilled: see runbook at <no value>"},
wantMissingVars: []string{"runbook_url"},
},
{
// Both title and body use unknown variables; MissingVars is the union of both.
name: "missing vars: unknown variables in both title and body",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "HighMemory", ruletypes.LabelSeverityName: "critical"}, nil, true),
},
input: alertmanagertypes.ExpandRequest{
TitleTemplate: "[$environment] $rule.name and [{{ $service }}]",
BodyTemplate: "$rule.name: see runbook at $runbook_url",
},
wantTitle: "[<no value>] HighMemory and [<no value>]",
wantBody: []string{"HighMemory: see runbook at <no value>"},
wantMissingVars: []string{"environment", "runbook_url", "service"},
},
{
// Custom title template that expands to only whitespace triggers the fallback,
// so the default title template is used instead.
name: "fallback: whitespace-only custom title falls back to default",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, false),
},
input: alertmanagertypes.ExpandRequest{
TitleTemplate: " ",
DefaultTitleTemplate: "{{ .CommonLabels.alertname }} ({{ .Status | toUpper }})",
DefaultBodyTemplate: "Runbook: https://runbook.example.com",
},
wantTitle: "HighCPU (RESOLVED)",
wantBody: []string{"Runbook: https://runbook.example.com"},
wantIsDefaultBody: true,
},
{
name: "using non-existing function in template",
alerts: []*types.Alert{
createAlert(map[string]string{ruletypes.LabelAlertName: "HighCPU", ruletypes.LabelSeverityName: "critical"}, nil, true),
},
input: alertmanagertypes.ExpandRequest{
TitleTemplate: "$rule.name ({{$severity | toUpperAndTrim}}) for $alertname",
},
errorContains: "function \"toUpperAndTrim\" not defined",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := at.Expand(ctx, tc.input, tc.alerts)
if tc.errorContains != "" {
require.ErrorContains(t, err, tc.errorContains)
return
}
require.NoError(t, err)
if tc.wantTitle != "" {
require.Equal(t, tc.wantTitle, got.Title)
}
if tc.wantBody != nil {
require.Equal(t, tc.wantBody, got.Body)
}
require.Equal(t, tc.wantIsDefaultBody, got.IsDefaultBody)
if len(tc.wantMissingVars) == 0 {
require.Empty(t, got.MissingVars)
} else {
sort.Strings(tc.wantMissingVars)
require.Equal(t, tc.wantMissingVars, got.MissingVars)
}
})
}
}

View File

@@ -0,0 +1,33 @@
package alertmanagertemplate
import (
"github.com/SigNoz/signoz/pkg/types/ruletypes"
"github.com/prometheus/alertmanager/types"
)
// ExtractTemplatesFromAnnotations pulls the user-authored title and body
// templates off the well-known annotation keys attached by the rule manager.
// A template is returned only if every alert in the group carries the same
// value under that key; otherwise the empty string is returned for that slot
// (which causes Expand to fall back to the channel default).
func ExtractTemplatesFromAnnotations(alerts []*types.Alert) (titleTemplate, bodyTemplate string) {
if len(alerts) == 0 {
return "", ""
}
title := string(alerts[0].Annotations[ruletypes.AnnotationTitleTemplate])
body := string(alerts[0].Annotations[ruletypes.AnnotationBodyTemplate])
for _, a := range alerts[1:] {
if title != "" && string(a.Annotations[ruletypes.AnnotationTitleTemplate]) != title {
title = ""
}
if body != "" && string(a.Annotations[ruletypes.AnnotationBodyTemplate]) != body {
body = ""
}
if title == "" && body == "" {
break
}
}
return title, body
}

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