Compare commits

..

33 Commits

Author SHA1 Message Date
Nikhil Soni
abefa35fde fix: handle exists expr after recent refactor 2026-08-11 22:17:14 +05:30
Nikhil Soni
5c5a4a7a3f Merge remote-tracking branch 'origin/main' into ns/scope 2026-08-11 17:34:30 +05:30
Nikhil Soni
883e9492d6 Merge remote-tracking branch 'origin/main' into ns/scope 2026-08-10 15:01:20 +05:30
Nikhil Soni
ab3b88966e Merge remote-tracking branch 'origin/main' into ns/scope
# Conflicts:
#	pkg/telemetryschema/tracestelemetryschema/field_mapper.go
#	tests/conftest.py
#	tests/integration/tests/querier/04_traces.py
2026-08-05 16:44:41 +05:30
Nikhil Soni
08ebc37109 fix: handle fixed scope.name and scope.version fields to work without scope 2026-07-02 19:04:41 +05:30
Nikhil Soni
aa5a1c5e62 chore: update collector version 2026-07-02 14:46:07 +05:30
Nikhil Soni
7b34a47ac5 chore: run formatting on tests 2026-07-02 14:41:33 +05:30
Nikhil Soni
b4b2d7bb66 test: add test to show cross context matching 2026-06-24 18:34:38 +05:30
Nikhil Soni
e16416475b refactor: drop unused fields 2026-06-24 18:07:47 +05:30
Nikhil Soni
0ea7c1ae6e test: add more cases for scope name 2026-06-24 18:05:50 +05:30
Nikhil Soni
a023c8ed4a test: add integration test for scope fields 2026-06-24 15:25:17 +05:30
Nikhil Soni
a73ae62cd1 Merge remote-tracking branch 'origin/main' into ns/scope 2026-06-24 13:02:18 +05:30
Nikhil Soni
ec6fb58052 chore: add more tests 2026-06-24 12:37:42 +05:30
Nikhil Soni
d3d13eb7ff fix: remove handling of normalized properties for scope
Otherwise it will be impossible to query if scope attribute also
exists with same name - name and version
2026-05-21 11:19:22 +05:30
Nikhil Soni
782de2b210 fix: use correct error type for internal issues 2026-05-20 15:32:26 +05:30
Nikhil Soni
d3c38693f3 fix: allow 'scope.' prefix for keys with other context 2026-05-20 15:30:25 +05:30
Nikhil Soni
8791df3697 fix: avoid removing context prefix to support attr with prefix 2026-05-19 19:14:23 +05:30
Nikhil Soni
eb719c3d0d fix: use key selector with context prefix 2026-05-19 18:56:00 +05:30
Nikhil Soni
f10435c210 Merge remote-tracking branch 'origin' into ns/scope 2026-05-19 13:55:36 +05:30
Nikhil Soni
f3f1e9cb59 chore: add tests for denormalized field name as well 2026-05-19 13:55:25 +05:30
Nikhil Soni
d0370ce3ef fix: handle fields with included context for scope (select clause) 2026-05-14 17:02:56 +05:30
Nikhil Soni
d169761e65 Merge remote-tracking branch 'origin/main' into ns/scope 2026-05-14 11:50:33 +05:30
Nikhil Soni
87864ef5d4 chore: remove duplicates from .gitignore 2026-05-11 15:45:32 +05:30
Nikhil Soni
2e0bc8998e chore: use name as key name for scope instead of scope.name 2026-05-11 15:40:45 +05:30
Nikhil Soni
7e1f4aa50d Merge remote-tracking branch 'origin/main' into ns/scope 2026-05-11 14:27:19 +05:30
Nikhil Soni
35da39247c Merge branch 'main' into ns/scope 2026-05-07 17:41:11 +05:30
Nikhil Soni
ceccc47a34 fix: fix test for case without resource filter 2026-05-07 16:04:03 +05:30
Nikhil Soni
23da5e22ec Merge branch 'main' into ns/scope 2026-05-07 13:27:34 +05:30
Nikhil Soni
4c1b479149 chore: add tests for scope fields 2026-04-28 20:27:10 +05:30
Nikhil Soni
f72204a8b2 refactor: simplify field mapper for scope 2026-04-28 20:26:37 +05:30
Nikhil Soni
deb3f385fa chore: remove underscore version of scope fields 2026-04-23 10:26:55 +05:30
Nikhil Soni
77ce5f86b1 fix: use scope as json field instead with name and version 2026-04-23 01:15:02 +05:30
Nikhil Soni
ff211de441 feat: add support for scope fields in traces 2026-04-14 10:45:08 +05:30
243 changed files with 3874 additions and 7164 deletions

View File

@@ -1499,7 +1499,6 @@ components:
- computeengine
- gke
- cloudstorage
- cloudsql_mysql
type: string
CloudintegrationtypesServiceMetadata:
properties:

View File

@@ -98,6 +98,14 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {
featureSet[idx].Active = true
}
}
}
ah.Respond(w, featureSet)
}

View File

@@ -17,3 +17,15 @@ func GetOrDefaultEnv(key string, fallback string) string {
}
return v
}
// constant functions that override env vars
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
var IsDotMetricsEnabled = false
func init() {
if GetOrDefaultEnv(DotMetricsEnabled, "true") == "true" {
IsDotMetricsEnabled = true
}
}

View File

@@ -487,11 +487,8 @@
// Simplifies boolean returns
"sonarjs/prefer-while": "error",
// Suggests while loops over for loops
"sonarjs/elseif-without-else": "off",
"sonarjs/elseif-without-else": "off"
// Requires final else in if-else-if chains (was disabled)
"signoz/no-conditional-text-nodes-with-siblings": "warn",
// Vendored from eslint-plugin-react-google-translate
"signoz/no-return-text-nodes": "warn"
},
"ignorePatterns": [
"src/parser/*.ts",

View File

@@ -16,7 +16,6 @@
"lint:generated": "oxlint ./src/api/generated --fix",
"lint:fix": "oxlint ./src --fix",
"lint:styles": "stylelint \"src/**/*.scss\"",
"test:plugins": "node --test \"plugins/__tests__/*.test.mjs\"",
"jest": "jest",
"jest:coverage": "jest --coverage",
"jest:watch": "jest --watch",
@@ -126,7 +125,6 @@
"rrule": "2.8.1",
"styled-components": "^5.3.11",
"timestamp-nano": "^1.0.0",
"translation-resilience": "^0.2.0",
"typescript": "5.9.3",
"uplot": "1.6.31",
"uuid": "14.0.1",

View File

@@ -1,130 +0,0 @@
# Plugin rule tests
Tests for the custom oxlint rules in `plugins/rules/`.
```bash
pnpm test:plugins
```
Runs on `node --test` rather than jest. The jest config is built for application
code — jsdom, ts-jest ESM transforms, a large `transformIgnorePatterns` wall —
and none of it applies to a suite whose only job is to shell out to the linter.
## Why it drives the real binary
Each case is written to a temp file and linted by the actual `oxlint` binary,
with every builtin category switched off so the only diagnostics that can appear
belong to the rule under test. Assertions therefore describe what CI enforces.
The alternative — walking the AST in-process — would need a stand-in for
oxlint's JS plugin AST. That AST is ESTree-shaped but not ESTree, and it carries
no type information, so a stand-in would drift from the runtime it claims to
model and the tests would certify behaviour that never happens.
All cases in a suite share one `oxlint` invocation and are mapped back by
filename. Per-case spawning costs roughly 80ms each; batching keeps both suites
together at around 250ms.
## Adding a suite
```js
import { ruleTester } from './rule-tester.mjs';
await ruleTester({
rule: 'no-navigator-clipboard',
valid: ['const x = 1;'],
invalid: [
{
code: 'navigator.clipboard.writeText("x");',
errors: [{ message: 'useCopyToClipboard', line: 1, column: 1 }],
},
],
});
```
`ruleTester` must be awaited at the top level — it loads the plugin and runs
`oxlint` before declaring the tests.
- `rule` — the key the plugin exports it under. `plugin` defaults to
`plugins/signoz.mjs`; pass a path relative to `frontend/` for another plugin.
- Cases are `.tsx` unless a `filename` gives another extension.
- `errors` takes a count or an array. Each entry may assert `message` (substring
or `RegExp`), `line` and `column`; omitted fields are not checked.
- `name` labels the case in the output and defaults to its first line of code.
- `output` asserts the source after suggestions are applied — see below.
- `todo` marks a case as a known defect — see below.
## Suggestions
Both Google Translate rules attach their wrap as a *suggestion*, not a fix, so
`--fix` leaves the code alone and `--fix-suggestions` applies it. The wrap is
`<span className="translate-safe">`, and `.translate-safe` is `display: contents`
in `src/styles.scss`: React owns an element that absorbs Translate's `<font>`
swap, while the box tree stays as it was, so a flex or grid parent still sees one
contiguous text run rather than a new item with its own `gap`.
It stays a suggestion because the element is still a DOM child even with no box:
`> *`, `:nth-child` and sibling selectors still count it, and a component that
inspects its children — `React.Children.map`, antd `Tooltip`, `Space` — sees an
element where a string used to be. That is what oxlint means by "May change
program behavior" in `--fix-suggestions --help`.
An invalid case carrying `output` is linted twice: once for diagnostics, and
once with `--fix-suggestions` over an untouched copy of the same files. The
second run costs one extra `oxlint` spawn per suite and only happens when at
least one case asks for it.
```js
{
code: "export const A = () => <div>{f ? 'a' : 'b'}<b/></div>;",
errors: 2,
output:
'export const A = () => <div>{f ? <span className="translate-safe">a</span> : <span className="translate-safe">b</span>}<b/></div>;',
}
```
Suggestions do not reformat, so a real run is `oxlint --fix-suggestions` then
`oxfmt`.
## Known defects
A case carrying `todo` asserts what the rule *should* do. It still runs, but a
failure is reported as a todo rather than failing the suite, so a bug can be
pinned as an executable spec instead of prose. Fixing the rule turns the todo
green; deleting the flag then makes it a regression guard.
Cases are prefixed `FP:` where the rule reports something it should not, `GAP:`
where it misses something it should catch, and `TYPE-AWARE:` where the miss is
only fixable once the linter can resolve types. Everything without a flag is a
characterisation test recording current behaviour.
The current todos:
**Gaps — constructs the rules never visit.** `isProblematicConditional` requires
a `JSXElement` parent, so a conditional inside a fragment is never inspected even
though the failure does not care about the parent's kind. `no-return-text-nodes`
listens only for `FunctionDeclaration` and reads the name off `node.id`, so
arrow-function components and anonymous default exports are invisible — this
codebase writes components as arrow functions, which is why that rule reports
nothing across `src`.
Class components are left out deliberately rather than pinned as a gap: there
are none in `src`.
**Type-aware gaps.** Upstream resolves branch types through
`@typescript-eslint/utils` and reports anything typed `string` or `number`.
oxlint's JS plugin runtime exposes no type information — `sourceCode.parserServices`
is always `{}` — so those code paths were removed rather than left dormant. The
`TYPE-AWARE:` todos record what they used to catch, and become the acceptance
criteria if oxlint ever hands types to JS plugins.
## Not a defect
Without types, `no-conditional-text-nodes-with-siblings` falls back to a callee
allowlist (`t`, `formatMessage`, `toString`, `toLocaleString`). Cases around
that allowlist pin its edges; widening it is the supported way to catch more
call expressions.
Both branches of a ternary are reported separately, so one fix can clear two
diagnostics. That inflates the count but every reported node is genuinely a text
node, so the cases assert both.

View File

@@ -1,302 +0,0 @@
import { ruleTester } from './rule-tester.mjs';
const CONDITIONAL = 'Conditionally rendered text nodes with siblings';
const PRECEDED = 'Text nodes which are preceded by a conditional expression';
await ruleTester({
rule: 'no-conditional-text-nodes-with-siblings',
valid: [
{
name: 'conditional text node without siblings',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? 'yes' : 'no'}\n\t</div>\n);",
},
{
name: 'boolean branches are not text',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? true : false}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'null branches are not text',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? null : null}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'element branches are already wrapped',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? <b>y</b> : <i>n</i>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'text node before the conditional is safe',
code:
'export const A = () => (\n\t<div>\n\t\tleading text\n\t\t{flag && <b>y</b>}\n\t</div>\n);',
},
{
name: 'member expression on the condition side is not rendered',
code:
'export const A = () => (\n\t<div>\n\t\t{obj.name && <b>y</b>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'binary comparison on the condition side is not rendered',
code:
"export const A = () => (\n\t<div>\n\t\t{obj.name === 'x' && <b>y</b>}\n\t\t<span>x</span>\n\t</div>\n);",
},
// An empty string renders no text node at all, so there is nothing for
// Google Translate to wrap and nothing for React to lose. Reporting it used
// to be the rule's most common false positive.
{
name: 'element branch with an empty-string fallback',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? <b>Free Trial</b> : ''}\n\t\t<span>s</span>\n\t</div>\n);",
},
{
name: 'both branches empty',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? '' : ''}\n\t\t<span>s</span>\n\t</div>\n);",
},
{
name: 'logical and with an empty-string right-hand side',
code:
"export const A = () => (\n\t<div>\n\t\t{flag && ''}\n\t\t<span>s</span>\n\t</div>\n);",
},
// A template literal is checked the same way as the quoted form, so `{' '}`
// and ``{` `}`` agree.
{
name: 'empty template literal branch',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? `` : ''}\n\t\t<span>s</span>\n\t</div>\n);",
},
{
name: 'whitespace-only template literal branch is skipped',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? ` ` : <b>x</b>}\n\t\t<span>s</span>\n\t</div>\n);',
},
],
invalid: [
{
name: 'string literal branches with an element sibling',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? 'yes' : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 19 },
],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">yes</span> : <span className="translate-safe">no</span>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'logical and with a string right-hand side',
code:
"export const A = () => (\n\t<div>\n\t\t{flag && 'yes'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [{ message: CONDITIONAL, line: 3, column: 12 }],
},
{
name: 'numeric literals render as text',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? 1 : 2}\n\t\t<span>x</span>\n\t</div>\n);',
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 15 },
],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">{1}</span> : <span className="translate-safe">{2}</span>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'template literal branch',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? `yes ${n}` : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 24 },
],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">{`yes ${n}`}</span> : <span className="translate-safe">no</span>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'member expression branch',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? obj.name : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 22 },
],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">{obj.name}</span> : <span className="translate-safe">no</span>}\n\t\t<span>x</span>\n\t</div>\n);',
},
{
name: 'a string needing escapes stays inside braces',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? "it\'s" : <b>x</b>}\n\t\t<span>s</span>\n\t</div>\n);',
errors: [{ message: CONDITIONAL, line: 3, column: 11 }],
output:
'export const A = () => (\n\t<div>\n\t\t{flag ? <span className="translate-safe">{"it\'s"}</span> : <b>x</b>}\n\t\t<span>s</span>\n\t</div>\n);',
},
{
name: 'optional chaining branch',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? obj?.deep?.name : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 29 },
],
},
{
name: 'nested ternary reports every text branch',
code:
"export const A = () => (\n\t<div>\n\t\t{a ? (b ? 'x' : 'y') : 'z'}\n\t\t<span>s</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 13 },
{ message: CONDITIONAL, line: 3, column: 19 },
{ message: CONDITIONAL, line: 3, column: 26 },
],
},
{
name: 'static text following a conditional',
code:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\ttrailing text\n\t</div>\n);',
errors: [{ message: PRECEDED, line: 3, column: 21 }],
// Only the visible run is wrapped; the surrounding newlines and tabs are
// formatting and must stay outside the element.
output:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t<span className="translate-safe">trailing text</span>\n\t</div>\n);',
},
{
name: 'conditional text plus trailing static text reports both kinds',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? 'a' : 'b'}\n\t\tliteral tail\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 17 },
{ message: PRECEDED, line: 3, column: 21 },
],
},
// The callee allowlist below is the untyped fallback. Without type
// information the rule can only recognise known string-returning helpers,
// so `t()` and `formatMessage()` are reported while an arbitrary call is
// not. These cases pin that boundary.
{
name: 't() branch is reported via the callee allowlist',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? t('key') : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 22 },
],
},
{
name: 'formatMessage() branch is reported via the callee allowlist',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? formatMessage({id:'k'}) : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 37 },
],
},
{
name: 'arbitrary call is not recognised, only the literal branch reports',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? getString() : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [{ message: CONDITIONAL, line: 3, column: 25 }],
},
{
name: 'bare identifier is not recognised, only the literal branch reports',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? name : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [{ message: CONDITIONAL, line: 3, column: 18 }],
},
{
name: 'toString() branch is reported',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? val.toString() : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 28 },
],
},
{
name: 'toLocaleString() branch is reported',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? val.toLocaleString() : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 34 },
],
},
{
name: 't() in its own container following a conditional',
code:
"export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t{t('key')}\n\t</div>\n);",
errors: [{ message: PRECEDED, line: 4, column: 4 }],
},
{
name: 'toString() in its own container following a conditional',
code:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t{val.toString()}\n\t</div>\n);',
errors: [{ message: PRECEDED, line: 4, column: 4 }],
// The whole container is replaced, so the result is not `{<span>{…}</span>}`.
output:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t<span className="translate-safe">{val.toString()}</span>\n\t</div>\n);',
},
{
name: 'whitespace-only string branch is skipped, the other branch reports',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? ' ' : 'x'}\n\t\t<span>s</span>\n\t</div>\n);",
errors: [{ message: CONDITIONAL, line: 3, column: 17 }],
},
{
name: 'template literal holding an expression is not blank',
code:
'export const A = () => (\n\t<div>\n\t\t{flag ? `${n}` : <b>x</b>}\n\t\t<span>s</span>\n\t</div>\n);',
errors: [{ message: CONDITIONAL, line: 3, column: 11 }],
},
// Upstream resolves branch types through `@typescript-eslint/utils` and
// reports anything typed `string` or `number`. oxlint's JS plugin runtime
// exposes no type information, so those paths were dropped and only the
// callee allowlist remains. Kept as todos: if oxlint ever hands types to JS
// plugins these become the acceptance criteria.
{
todo: 'needs type information to know the call returns a string',
name: 'TYPE-AWARE: call returning a string',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? getString() : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 25 },
],
},
{
todo: 'needs type information to know the identifier is a string',
name: 'TYPE-AWARE: identifier holding a string',
code:
"export const A = () => (\n\t<div>\n\t\t{flag ? name : 'no'}\n\t\t<span>x</span>\n\t</div>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 18 },
],
},
{
todo: 'needs type information to know the identifier is a string',
name: 'TYPE-AWARE: string identifier following a conditional',
code:
'export const A = () => (\n\t<div>\n\t\t{flag && <b>y</b>}\n\t\t{label}\n\t</div>\n);',
errors: [{ message: PRECEDED, line: 4, column: 4 }],
},
// `isChildOfJSXElement` matches only `JSXElement`, so a fragment parent is
// never inspected. The Google Translate failure does not care whether the
// parent is an element or a fragment.
{
todo: 'fragment parents are never inspected',
name: 'GAP: conditional text with a sibling inside a fragment',
code:
"export const A = () => (\n\t<>\n\t\t{flag ? 'yes' : 'no'}\n\t\t<span>x</span>\n\t</>\n);",
errors: [
{ message: CONDITIONAL, line: 3, column: 11 },
{ message: CONDITIONAL, line: 3, column: 19 },
],
},
],
});

View File

@@ -1,162 +0,0 @@
import { ruleTester } from './rule-tester.mjs';
const RETURNS_TEXT = 'React components should avoid returning text nodes';
await ruleTester({
rule: 'no-return-text-nodes',
valid: [
{
name: 'lowercase function is not a component',
code: "export function foo() {\n\treturn 'text';\n}",
},
{
name: 'returning JSX',
code: 'export function Foo() {\n\treturn <div>hi</div>;\n}',
},
{
name: 'returning null',
code: 'export function Foo() {\n\treturn null;\n}',
},
{
name: 'returning boolean',
code: 'export function Foo() {\n\treturn true;\n}',
},
{ name: 'bare return', code: 'export function Foo() {\n\treturn;\n}' },
{
name: 'returning a variable is not a literal',
code: "export function Foo() {\n\tconst s = 'x';\n\treturn s;\n}",
},
{
name: 'lowercase nested function inside a component',
code:
"export function Foo() {\n\tfunction helper() {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
},
{
// The repo has no class components, so this is out of scope rather than
// a gap worth closing.
name: 'class method',
code: "export class Foo {\n\trender() {\n\t\treturn 'text';\n\t}\n}",
},
],
invalid: [
{
name: 'string literal',
code: "export function Foo() {\n\treturn 'text';\n}",
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
output:
'export function Foo() {\n\treturn <span className="translate-safe">{\'text\'}</span>;\n}',
},
{
// JSX does not parse in a `.ts` file, so no suggestion is offered there.
name: 'string literal in a non-JSX file',
filename: 'case.ts',
code: "export function Foo() {\n\treturn 'text';\n}",
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
output: "export function Foo() {\n\treturn 'text';\n}",
},
{
name: 'numeric literal',
code: 'export function Foo() {\n\treturn 42;\n}',
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
},
{
name: 'template literal',
code: 'export function Foo() {\n\treturn `text ${x}`;\n}',
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
},
{
name: 'inside an if consequent',
code:
"export function Foo() {\n\tif (a) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside an else block',
code:
"export function Foo() {\n\tif (a) {\n\t\treturn <div/>;\n\t} else {\n\t\treturn 'x';\n\t}\n}",
errors: [{ message: RETURNS_TEXT, line: 5, column: 3 }],
},
{
name: 'inside an else-if chain',
code:
"export function Foo() {\n\tif (a) {\n\t\treturn <div/>;\n\t} else if (b) {\n\t\treturn 'x';\n\t}\n\treturn null;\n}",
errors: [{ message: RETURNS_TEXT, line: 5, column: 3 }],
},
{
name: 'inside a switch case',
code:
"export function Foo() {\n\tswitch (a) {\n\t\tcase 1:\n\t\t\treturn 'x';\n\t\tdefault:\n\t\t\treturn <div/>;\n\t}\n}",
errors: [{ message: RETURNS_TEXT, line: 4, column: 4 }],
},
{
name: 'inside try, catch and finally',
code:
"export function Foo() {\n\ttry {\n\t\treturn 'a';\n\t} catch {\n\t\treturn 'b';\n\t} finally {\n\t\treturn 'c';\n\t}\n}",
errors: [
{ message: RETURNS_TEXT, line: 3, column: 3 },
{ message: RETURNS_TEXT, line: 5, column: 3 },
{ message: RETURNS_TEXT, line: 7, column: 3 },
],
},
{
name: 'inside a for loop',
code:
"export function Foo() {\n\tfor (let i = 0; i < 3; i++) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside a for-of loop',
code:
"export function Foo() {\n\tfor (const i of list) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside a for-in loop',
code:
"export function Foo() {\n\tfor (const k in obj) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside a while loop',
code:
"export function Foo() {\n\twhile (a) {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'inside a do-while loop',
code: "export function Foo() {\n\tdo {\n\t\treturn 'x';\n\t} while (a);\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
{
name: 'capitalised nested function is treated as a component',
code:
"export function Foo() {\n\tfunction Helper() {\n\t\treturn 'x';\n\t}\n\treturn <div/>;\n}",
errors: [{ message: RETURNS_TEXT, line: 3, column: 3 }],
},
// The rule listens only for `FunctionDeclaration` and reads the component
// name off `node.id`. Everything below returns a text node from something
// React renders as a component, and none of it is reported. This codebase
// writes components as arrow functions, which is why the rule currently
// finds nothing in `src`.
{
todo: 'arrow function components are never visited',
name: 'GAP: arrow component with an expression body',
code: "export const Foo = () => 'text';",
errors: 1,
},
{
todo: 'arrow function components are never visited',
name: 'GAP: arrow component with a block body',
code: "export const Foo = () => {\n\treturn 'text';\n};",
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
},
{
todo: 'anonymous declarations have no node.id to read a name from',
name: 'GAP: anonymous default-exported component',
code: "export default function () {\n\treturn 'text';\n}",
errors: [{ message: RETURNS_TEXT, line: 2, column: 2 }],
},
],
});

View File

@@ -1,257 +0,0 @@
/**
* Test harness for oxlint JS plugins.
*
* Rules are exercised through the real `oxlint` binary rather than a hand-rolled
* AST walker, so what the tests assert is exactly what CI enforces. oxlint's JS
* plugin AST is close to ESTree but not identical, and it exposes no type
* information, so any in-process fake would drift from the real runtime.
*
* All cases in a suite are written to one temp directory and linted in a single
* oxlint invocation, then mapped back by filename. Spawning per case costs ~80ms
* each; batching keeps a full suite under a second.
*/
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import {
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath, pathToFileURL } from 'node:url';
const FRONTEND_DIR = path.resolve(fileURLToPath(import.meta.url), '../../..');
const OXLINT_BIN = path.join(FRONTEND_DIR, 'node_modules/.bin/oxlint');
// oxlint enables its default categories unless every one is switched off, and a
// stray builtin diagnostic would be indistinguishable from the rule under test.
const CATEGORIES_OFF = {
correctness: 'off',
suspicious: 'off',
pedantic: 'off',
perf: 'off',
style: 'off',
restriction: 'off',
nursery: 'off',
};
function normaliseCase(entry, index) {
const testCase = typeof entry === 'string' ? { code: entry } : entry;
const extension = testCase.filename
? path.extname(testCase.filename).slice(1)
: 'tsx';
return {
...testCase,
index,
basename: `case-${String(index).padStart(3, '0')}.${extension}`,
};
}
function diagnosticFilename(diagnostic) {
const raw = diagnostic.filename ?? '';
const asPath = raw.startsWith('file://') ? fileURLToPath(raw) : raw;
return path.basename(asPath);
}
function toError(diagnostic) {
const span = diagnostic.labels?.[0]?.span;
return {
message: diagnostic.message,
line: span?.line,
column: span?.column,
};
}
function runOxlint(dir, configPath, extraArgs = []) {
const args = ['--config', configPath, '--format', 'json', ...extraArgs, '.'];
try {
return execFileSync(OXLINT_BIN, args, {
cwd: dir,
encoding: 'utf8',
// A rule that reports on every case can produce a lot of output.
maxBuffer: 64 * 1024 * 1024,
});
} catch (error) {
// oxlint exits non-zero whenever it reports a diagnostic, which is the
// expected outcome for every `invalid` case.
if (typeof error.stdout === 'string' && error.stdout.trim() !== '') {
return error.stdout;
}
throw new Error(`oxlint failed to run:\n${error.stderr || error.message}`, {
cause: error,
});
}
}
function writeSuite(dir, cases, { pluginPath, ruleId }) {
for (const testCase of cases) {
const target = path.join(dir, testCase.basename);
mkdirSync(path.dirname(target), { recursive: true });
writeFileSync(target, testCase.code);
}
const configPath = path.join(dir, '.oxlintrc.json');
writeFileSync(
configPath,
JSON.stringify({
jsPlugins: [pluginPath],
categories: CATEGORIES_OFF,
rules: { [ruleId]: 'error' },
}),
);
return configPath;
}
/**
* Lints every case in one pass, and applies suggestions in a second pass over an
* untouched copy when any case declares `output`.
*
* @returns {{errors: Map<string, object[]>, outputs: Map<string, string>}}
*/
function lintCases(cases, options) {
const root = mkdtempSync(path.join(tmpdir(), 'oxlint-rule-tester-'));
try {
const lintDir = path.join(root, 'lint');
mkdirSync(lintDir);
const report = JSON.parse(
runOxlint(lintDir, writeSuite(lintDir, cases, options)),
);
const errors = new Map(cases.map((testCase) => [testCase.basename, []]));
for (const diagnostic of report.diagnostics ?? []) {
const bucket = errors.get(diagnosticFilename(diagnostic));
// oxlint reports config-level problems without a filename; surfacing
// them as a suite failure beats silently testing nothing.
if (!bucket) {
throw new Error(`Unexpected diagnostic: ${diagnostic.message}`);
}
bucket.push(toError(diagnostic));
}
const outputs = new Map();
if (cases.some((testCase) => testCase.output !== undefined)) {
const fixDir = path.join(root, 'fix');
mkdirSync(fixDir);
runOxlint(fixDir, writeSuite(fixDir, cases, options), ['--fix-suggestions']);
for (const testCase of cases) {
outputs.set(
testCase.basename,
readFileSync(path.join(fixDir, testCase.basename), 'utf8'),
);
}
}
return { errors, outputs };
} finally {
rmSync(root, { recursive: true, force: true });
}
}
function assertMessage(actual, expected, label) {
if (expected instanceof RegExp) {
assert.match(actual, expected, label);
} else {
assert.ok(
actual.includes(expected),
`${label}\n expected message to contain: ${expected}\n actual: ${actual}`,
);
}
}
function assertErrors(actual, expected, code) {
const context = `\n--- code ---\n${code}\n--- reported ---\n${JSON.stringify(actual, null, 2)}`;
if (typeof expected === 'number') {
assert.equal(actual.length, expected, `error count${context}`);
return;
}
assert.equal(actual.length, expected.length, `error count${context}`);
expected.forEach((want, i) => {
const got = actual[i];
if (want.message !== undefined) {
assertMessage(got.message, want.message, `error[${i}] message${context}`);
}
if (want.line !== undefined) {
assert.equal(got.line, want.line, `error[${i}] line${context}`);
}
if (want.column !== undefined) {
assert.equal(got.column, want.column, `error[${i}] column${context}`);
}
});
}
/**
* Declares a suite for one rule.
*
* A case carrying `todo` asserts the behaviour the rule *should* have. It still
* runs, but a failure is reported as a todo instead of failing the suite, so a
* known bug can be pinned as an executable spec. Delete the flag once the rule
* is fixed and the case starts guarding the fix.
*
* An invalid case carrying `output` also asserts the source after
* `--fix-suggestions` has been applied.
*
* @param {object} options
* @param {string} options.rule - rule name as exported by the plugin
* @param {string} [options.plugin] - path to the plugin, relative to `frontend/`
* @param {Array<string | {code: string, name?: string, filename?: string, todo?: string}>} options.valid
* @param {Array<{code: string, name?: string, filename?: string, todo?: string, output?: string, errors: number | Array<{message?: string | RegExp, line?: number, column?: number}>}>} options.invalid
*/
export async function ruleTester({
rule,
plugin = 'plugins/signoz.mjs',
valid = [],
invalid = [],
}) {
const pluginPath = path.join(FRONTEND_DIR, plugin);
const { default: pluginModule } = await import(pathToFileURL(pluginPath));
assert.ok(
pluginModule.rules?.[rule],
`plugin ${plugin} does not export a rule named "${rule}"`,
);
const ruleId = `${pluginModule.meta.name}/${rule}`;
const validCases = valid.map(normaliseCase);
const invalidCases = invalid.map((entry, i) =>
normaliseCase(entry, valid.length + i),
);
const { errors, outputs } = lintCases([...validCases, ...invalidCases], {
pluginPath,
ruleId,
});
const declare = (t, testCase, expected) => {
const label = testCase.name ?? testCase.code.trim().split('\n')[0];
return t.test(label, { todo: testCase.todo }, () => {
assertErrors(errors.get(testCase.basename), expected, testCase.code);
if (testCase.output !== undefined) {
assert.equal(
outputs.get(testCase.basename),
testCase.output,
`suggestion output\n--- code ---\n${testCase.code}`,
);
}
});
};
test(ruleId, async (t) => {
await t.test('valid', async (t) => {
for (const testCase of validCases) {
await declare(t, testCase, 0);
}
});
await t.test('invalid', async (t) => {
for (const testCase of invalidCases) {
await declare(t, testCase, testCase.errors);
}
});
});
}

View File

@@ -1,313 +0,0 @@
/**
* Rule: no-conditional-text-nodes-with-siblings
*
* Conditionally rendered text nodes with siblings should be wrapped in an
* element (for example a `<span>`), otherwise Google Translate causes a browser
* error. Translate replaces the text node with a `<font>` wrapper, React still
* holds a reference to the original node, and the next render throws on
* `removeChild`.
*
* Adapted from https://github.com/getcouped/eslint-plugin-react-google-translate
* (v1.0.4). The upstream rule resolves branch types through
* `@typescript-eslint/utils`; oxlint's JS plugin runtime exposes no type
* information, so those paths are dropped and call expressions are matched
* against the allowlist below instead.
*/
// Calls known to render as text. Without types this is the only way to
// recognise a string-returning call; widen it to catch more helpers.
const TEXT_RETURNING_CALLEES = new Set(['formatMessage', 't']);
const STRINGIFY_METHODS = new Set(['toString', 'toLocaleString']);
function calleeName(node) {
return node.type === 'Identifier' ? node.name : null;
}
function isTextReturningCall(node) {
const { callee } = node;
if (TEXT_RETURNING_CALLEES.has(calleeName(callee))) {
return node.arguments.length > 0;
}
if (callee.type === 'MemberExpression' && !callee.computed) {
return STRINGIFY_METHODS.has(calleeName(callee.property));
}
return STRINGIFY_METHODS.has(calleeName(callee));
}
/**
* True when the node renders no visible text. An empty or whitespace-only value
* produces no DOM text node, so Google Translate has nothing to wrap and React
* nothing to lose.
*/
function isBlankText(node) {
if (node.type === 'Literal' || node.type === 'JSXText') {
return typeof node.value === 'string' && node.value.trim() === '';
}
if (node.type === 'TemplateLiteral') {
return (
node.expressions.length === 0 &&
node.quasis.every((quasi) => (quasi.value.cooked ?? '').trim() === '')
);
}
return false;
}
function isConditionallyRendered(node) {
const parent = node.parent;
return (
parent?.type === 'ConditionalExpression' ||
parent?.type === 'LogicalExpression'
);
}
function isRenderedConditional(node) {
return (
node.type === 'JSXExpressionContainer' &&
(node.expression?.type === 'ConditionalExpression' ||
node.expression?.type === 'LogicalExpression')
);
}
/** Children that produce output, i.e. everything but formatting whitespace. */
function renderedChildren(node) {
const children = node?.children;
if (!children) {
return null;
}
return children.filter((child) => !isBlankText(child));
}
/** True when `node` is a JSX child rendered alongside at least one other child. */
function hasSiblings(node) {
if (!(node?.parent?.children?.length > 1)) {
return false;
}
return renderedChildren(node.parent).some((child) => child !== node);
}
function isPrecededByConditional(node) {
const children = renderedChildren(node?.parent);
if (!children) {
return false;
}
return children.some(
(child) => child.start < node.start && isRenderedConditional(child),
);
}
/** Walk out of nested conditionals so nested branches report against the outer container. */
function getOutermostConditional(node) {
let current = node;
while (isConditionallyRendered(current)) {
current = current.parent;
}
return current;
}
/** True when `node` is a conditional branch rendered directly beside other JSX children. */
function isProblematicConditional(node) {
if (!isConditionallyRendered(node)) {
return false;
}
const container = getOutermostConditional(node);
return (
container.parent?.type === 'JSXExpressionContainer' &&
container.parent.parent?.type === 'JSXElement' &&
hasSiblings(container.parent)
);
}
/** True when `node` renders after a sibling conditional, i.e. the DOM order Translate breaks. */
function followsConditionalSibling(node) {
return (
node.parent?.parent?.type === 'JSXElement' &&
hasSiblings(node.parent) &&
isPrecededByConditional(node.parent)
);
}
/**
* `A && B` and the test of a ternary are conditions, not rendered output.
*/
function isCondition(node) {
let current = node;
while (current.parent?.type === 'LogicalExpression') {
if (current.parent.left === current) {
return true;
}
current = current.parent;
}
if (current.parent?.type === 'ConditionalExpression') {
return current.parent.test === current;
}
return false;
}
function isConditionOperand(node) {
if (node.parent?.type === 'BinaryExpression') {
return isCondition(node.parent);
}
return isCondition(node);
}
// A string may only be inlined as JSX text when it needs no escaping and no
// whitespace of its own: JSX collapses leading and trailing whitespace, and
// these characters would either close the element or start an entity.
const NEEDS_BRACES = /['"{}<>&\r\n]/;
// `display: contents`, declared in src/styles.scss. React owns the element so
// Translate's `<font>` swap is absorbed, while the box tree stays as it was and
// a flex or grid parent still sees one contiguous text run.
const OPEN = '<span className="translate-safe">';
const CLOSE = '</span>';
/** Wraps the reported expression so React owns an element Translate cannot replace. */
function wrapExpression(fixer, sourceCode, node) {
// A call reported on its own already sits in a container. Replacing the
// container yields `<span …>{expr}</span>` rather than `{<span …>{expr}</span>}`.
const target =
node.parent?.type === 'JSXExpressionContainer' ? node.parent : node;
if (
node.type === 'Literal' &&
typeof node.value === 'string' &&
!NEEDS_BRACES.test(node.value) &&
node.value.trim() === node.value
) {
return fixer.replaceText(target, `${OPEN}${node.value}${CLOSE}`);
}
return fixer.replaceText(
target,
`${OPEN}{${sourceCode.getText(node)}}${CLOSE}`,
);
}
/**
* Wraps static JSX text. Only the visible run is wrapped: the node also spans
* the formatting whitespace around it, which has to stay outside the element.
*/
function wrapJsxText(fixer, node) {
const raw = node.value;
const leading = raw.length - raw.trimStart().length;
const trailing = raw.length - raw.trimEnd().length;
return fixer.replaceTextRange(
[node.start + leading, node.end - trailing],
`${OPEN}${raw.trim()}${CLOSE}`,
);
}
export default {
meta: {
type: 'problem',
docs: {
description:
'Conditionally rendered text nodes should be wrapped in an element (for example a `<span>`), otherwise Google Translate can cause a browser error.',
url: 'https://github.com/getcouped/eslint-plugin-react-google-translate#eslint-plugin-react-google-translate',
},
schema: [],
// Wrapping adds a DOM element, which can turn into a flex/grid item or
// break `> *` and `:nth-child` selectors, so it is offered as a suggestion
// (`--fix-suggestions`) rather than applied by a bare `--fix`.
hasSuggestions: true,
messages: {
'conditional-text-node':
'Conditionally rendered text nodes with siblings, rendered as a direct child of a JSX element, must be wrapped so Google Translate cannot break React\'s DOM: `<span className="translate-safe">{value}</span>`. Translate replaces the bare text node with a `<font>` element and React then throws on `removeChild`. This also applies to values returned from functions, so `getString()` becomes `<span className="translate-safe">{getString()}</span>`.',
'text-node-preceded-by-conditional':
'Text nodes which are preceded by a conditional expression, rendered as a direct child of a JSX element, must be wrapped so Google Translate cannot break React\'s DOM: `<span className="translate-safe">text</span>`. Translate replaces the bare text node with a `<font>` element and React then throws on `removeChild`.',
},
},
createOnce(context) {
const suggestWrap = (build) => [{ desc: 'Wrap in a <span>', fix: build }];
const wrap = (fixer, node) => wrapExpression(fixer, context.sourceCode, node);
const reportConditional = (node) => {
context.report({
node,
messageId: 'conditional-text-node',
suggest: suggestWrap((fixer) => wrap(fixer, node)),
});
};
const reportPreceded = (node, build) => {
context.report({
node,
messageId: 'text-node-preceded-by-conditional',
suggest: suggestWrap(build),
});
};
return {
// String and numeric branches: `{flag ? 'yes' : 'no'}`
Literal(node) {
if (node.value === null || typeof node.value === 'boolean') {
return;
}
if (isBlankText(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
},
TemplateLiteral(node) {
if (isBlankText(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
},
// Static text rendered after a conditional: `{flag && <b/>}trailing`
JSXText(node) {
if (isBlankText(node)) {
return;
}
if (hasSiblings(node) && isPrecededByConditional(node)) {
reportPreceded(node, (fixer) => wrapJsxText(fixer, node));
}
},
CallExpression(node) {
if (isCondition(node) || !isTextReturningCall(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
if (followsConditionalSibling(node)) {
reportPreceded(node, (fixer) => wrap(fixer, node));
}
},
// Values read off an object: `{flag ? user.name : 'anonymous'}`
MemberExpression(node) {
if (isConditionOperand(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
},
// Optional chaining wraps the member expression: `{flag ? a?.b?.c : 'x'}`
ChainExpression(node) {
if (isConditionOperand(node)) {
return;
}
if (isProblematicConditional(node)) {
reportConditional(node);
}
},
};
},
};

View File

@@ -1,115 +0,0 @@
/**
* Rule: no-return-text-nodes
*
* React components should not return a bare text node. Google Translate keeps
* displaying the stale translated text after a state change and nothing throws,
* which makes the bug very hard to track down. Numbers count too: JSX renders
* them as text.
*
* Adapted from https://github.com/getcouped/eslint-plugin-react-google-translate
* (v1.0.4). Upstream walks the function body statement by statement; this
* version visits `ReturnStatement` directly and walks up to the enclosing
* function, which covers the same constructs without enumerating them.
*/
const FUNCTION_TYPES = new Set([
'FunctionDeclaration',
'FunctionExpression',
'ArrowFunctionExpression',
]);
function isTextNode(node) {
if (!node) {
return false;
}
if (node.type === 'TemplateLiteral') {
return true;
}
return (
node.type === 'Literal' &&
(typeof node.value === 'string' || typeof node.value === 'number')
);
}
function getEnclosingFunction(node) {
let current = node.parent;
while (current) {
if (FUNCTION_TYPES.has(current.type)) {
return current;
}
current = current.parent;
}
return null;
}
function isComponentName(name) {
return (
typeof name === 'string' && name !== '' && name[0] === name[0].toUpperCase()
);
}
// The suggestion introduces JSX, which only parses in a JSX-enabled file.
function allowsJsx(filename) {
return filename.endsWith('.tsx') || filename.endsWith('.jsx');
}
export default {
meta: {
type: 'problem',
docs: {
description:
'React components should avoid returning text nodes directly (or numerical values which will be rendered as text). When a React component returns values other than JSX / null, Google Translate can continue to display stale values after state changes, without any error being thrown. Since this is very hard to debug it is better to avoid it altogether.',
url: 'https://github.com/getcouped/eslint-plugin-react-google-translate#eslint-plugin-react-google-translate',
},
schema: [],
// Wrapping changes what the component renders, so it is offered as a
// suggestion (`--fix-suggestions`) rather than applied by a bare `--fix`.
hasSuggestions: true,
messages: {
'return-value-is-text-node':
'React components should avoid returning text nodes directly (or numerical values which will be rendered as text). When a React component returns values other than JSX / null, Google Translate can continue to display stale values after state changes, without any error being thrown. Since this is very hard to debug it is better to avoid it altogether.',
},
},
createOnce(context) {
const buildSuggestion = (node) => {
if (!allowsJsx(context.filename)) {
return undefined;
}
return [
{
desc: 'Wrap in a <span>',
fix: (fixer) =>
fixer.replaceText(
node.argument,
`<span className="translate-safe">{${context.sourceCode.getText(node.argument)}}</span>`,
),
},
];
};
return {
ReturnStatement(node) {
if (!isTextNode(node.argument)) {
return;
}
// Only named function declarations are recognised as components, so a
// text return from a nested helper or a class method is left alone.
const fn = getEnclosingFunction(node);
if (fn?.type !== 'FunctionDeclaration') {
return;
}
if (!isComponentName(fn.id?.name)) {
return;
}
context.report({
node,
messageId: 'return-value-is-text-node',
suggest: buildSuggestion(node),
});
},
};
},
};

View File

@@ -13,8 +13,6 @@ import noAntdComponents from './rules/no-antd-components.mjs';
import noSignozhqUiBarrel from './rules/no-signozhq-ui-barrel.mjs';
import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root.mjs';
import noConditionalTextNodesWithSiblings from './rules/no-conditional-text-nodes-with-siblings.mjs';
import noReturnTextNodes from './rules/no-return-text-nodes.mjs';
export default {
meta: {
@@ -29,7 +27,5 @@ export default {
'no-signozhq-ui-barrel': noSignozhqUiBarrel,
'no-css-module-bracket-access': noCssModuleBracketAccess,
'no-dashboard-fetch-outside-root': noDashboardFetchOutsideRoot,
'no-conditional-text-nodes-with-siblings': noConditionalTextNodesWithSiblings,
'no-return-text-nodes': noReturnTextNodes,
},
};

View File

@@ -294,9 +294,6 @@ importers:
timestamp-nano:
specifier: ^1.0.0
version: 1.0.1
translation-resilience:
specifier: ^0.2.0
version: 0.2.0
typescript:
specifier: 5.9.3
version: 5.9.3
@@ -8478,9 +8475,6 @@ packages:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'}
translation-resilience@0.2.0:
resolution: {integrity: sha512-IxTjhpHGp1SJxVEEPBu/YbBaHnymMRJdYxUY1i5qtYADuz9b8fdWhZSE/EeRS2aVIiuQjRqtYDk69ruS/3fzTg==}
trim-lines@3.0.1:
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
@@ -18238,8 +18232,6 @@ snapshots:
dependencies:
punycode: 2.3.1
translation-resilience@0.2.0: {}
trim-lines@3.0.1: {}
trough@2.1.0: {}

View File

@@ -388,10 +388,6 @@ function App(): JSX.Element {
if (error?.name === 'AbortError') {
return null;
}
// Ignore benign Monaco cancellation errors (name 'Canceled').
if (error?.name === 'Canceled') {
return null;
}
// Drop the event if its level is 'warning' or 'info'
if (event.level === 'warning' || event.level === 'info') {

View File

@@ -2818,7 +2818,6 @@ export enum CloudintegrationtypesServiceIDDTO {
computeengine = 'computeengine',
gke = 'gke',
cloudstorage = 'cloudstorage',
cloudsql_mysql = 'cloudsql_mysql',
}
export type CloudintegrationtypesCloudIntegrationServiceDTOAnyOf = {
/**

View File

@@ -24,3 +24,19 @@ export const Logout = async (): Promise<void> => {
window.dispatchEvent(new CustomEvent('LOGOUT'));
history.push(ROUTES.LOGIN);
};
export const UnderscoreToDotMap: Record<string, string> = {
k8s_cluster_name: 'k8s.cluster.name',
k8s_cluster_uid: 'k8s.cluster.uid',
k8s_namespace_name: 'k8s.namespace.name',
k8s_node_name: 'k8s.node.name',
k8s_node_uid: 'k8s.node.uid',
k8s_pod_name: 'k8s.pod.name',
k8s_pod_uid: 'k8s.pod.uid',
k8s_deployment_name: 'k8s.deployment.name',
k8s_daemonset_name: 'k8s.daemonset.name',
k8s_statefulset_name: 'k8s.statefulset.name',
k8s_cronjob_name: 'k8s.cronjob.name',
k8s_job_name: 'k8s.job.name',
k8s_persistentvolumeclaim_name: 'k8s.persistentvolumeclaim.name',
};

View File

@@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/user/setInvite';
/**
* @deprecated Use the generated `useCreateInvite` hook (or `createInvite` fetcher) from
* `api/generated/services/users` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const sendInvite = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>(`/invite`, {
...props,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default sendInvite;

View File

@@ -10,10 +10,6 @@ jest.mock('providers/Timezone', () => ({
}),
}));
jest.mock('providers/App/App', () => ({
useAppContext: (): { featureFlags: [] } => ({ featureFlags: [] }),
}));
const field = (name: string, type = ''): IField => ({
name,
type,

View File

@@ -2,15 +2,13 @@ import type { ReactElement } from 'react';
import { useMemo } from 'react';
import TanStackTable from 'components/TanStackTableView';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { FeatureKeys } from 'constants/features';
import {
getBodyDisplayString,
getSanitizedLogBody,
} from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { getLogFieldValue } from 'lib/logs/flatLogData';
import { useAppContext } from 'providers/App/App';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
@@ -28,10 +26,6 @@ export function useLogsTableColumns({
fontSize,
}: UseLogsTableColumnsProps): TableColumnDef<ILog>[] {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const { featureFlags } = useAppContext();
const isBodyJsonEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
?.active || false;
return useMemo<TableColumnDef<ILog>[]>(() => {
const stateIndicatorCol: TableColumnDef<ILog> = {
@@ -94,8 +88,7 @@ export function useLogsTableColumns({
const makeUserFieldCol = (f: IField): TableColumnDef<ILog> => ({
id: buildCompositeKey(f.name, f.type),
header: f.name,
accessorFn: (log): unknown =>
getLogFieldValue(log, f.name, isBodyJsonEnabled),
accessorFn: (log): unknown => FlatLogData(log)[f.name],
enableRemove: true,
width: { min: 192 },
cell: ({ value }): ReactElement => (
@@ -122,5 +115,5 @@ export function useLogsTableColumns({
.filter((c): c is TableColumnDef<ILog> => c !== null);
return [stateIndicatorCol, ...fieldCols];
}, [fields, fontSize, formatTimezoneAdjustedTimestamp, isBodyJsonEnabled]);
}, [fields, fontSize, formatTimezoneAdjustedTimestamp]);
}

View File

@@ -59,7 +59,6 @@ import {
dedupeOptionsByLabel,
getFieldContextPrefix,
getRecentOptions,
isSupportedFunction,
renderRecentDeleteButton,
} from './utils';
@@ -1276,13 +1275,11 @@ function QuerySearch({
}
if (queryContext.isInFunction) {
options = Object.values(QUERY_BUILDER_FUNCTIONS)
.filter((option) => isSupportedFunction(option, dataSource))
.map((option) => ({
label: option,
apply: `${option}()`,
type: 'function',
}));
options = Object.values(QUERY_BUILDER_FUNCTIONS).map((option) => ({
label: option,
apply: `${option}()`,
type: 'function',
}));
// Add space after selection for functions
const optionsWithSpace = addSpaceToOptions(options);

View File

@@ -1,12 +1,8 @@
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
import { DataSource } from 'types/common/queryBuilder';
import {
combineInitialAndUserExpression,
dedupeOptionsByLabel,
getFieldContextPrefix,
getUserExpressionFromCombined,
isSupportedFunction,
} from '../utils';
describe('entityLogsExpression', () => {
@@ -122,19 +118,3 @@ describe('dedupeOptionsByLabel', () => {
expect(dedupeOptionsByLabel([])).toStrictEqual([]);
});
});
describe('isSupportedFunction', () => {
const { HASANY, SEARCH } = QUERY_BUILDER_FUNCTIONS;
it('allows the has family on every signal', () => {
[DataSource.LOGS, DataSource.TRACES, DataSource.METRICS].forEach((signal) => {
expect(isSupportedFunction(HASANY, signal)).toBe(true);
});
});
it('allows search on logs only', () => {
expect(isSupportedFunction(SEARCH, DataSource.LOGS)).toBe(true);
expect(isSupportedFunction(SEARCH, DataSource.TRACES)).toBe(false);
expect(isSupportedFunction(SEARCH, DataSource.METRICS)).toBe(false);
});
});

View File

@@ -1,7 +1,6 @@
import { closeCompletion, startCompletion } from '@codemirror/autocomplete';
import type { Completion } from '@codemirror/autocomplete';
import type { EditorView } from '@uiw/react-codemirror';
import { QUERY_BUILDER_FUNCTIONS } from 'constants/antlrQueryConstants';
import dayjs from 'dayjs';
import { normalizeFilterExpression } from 'lib/recentQueries/normalize';
import * as recentQueriesStore from 'lib/recentQueries/recentQueriesStore';
@@ -16,15 +15,6 @@ import {
RECENTS_SECTION,
} from './constants';
// search() lives in the logs condition builder only; traces and metrics reject it
// as an unsupported operator. Every other function is implemented for all signals.
export function isSupportedFunction(
functionName: string,
signal: SignalType,
): boolean {
return functionName !== QUERY_BUILDER_FUNCTIONS.SEARCH || signal === 'logs';
}
export interface FieldContextPrefixMatch {
context: string;
remainder: string;

View File

@@ -1,280 +0,0 @@
import {
completionStatus,
currentCompletions,
startCompletion,
} from '@codemirror/autocomplete';
import { EditorView } from '@uiw/react-codemirror';
import { initialQueriesMap } from 'constants/queryBuilder';
import * as recentQueriesStore from 'lib/recentQueries/recentQueriesStore';
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
import { DataSource } from 'types/common/queryBuilder';
import { RECENTS_DISPLAY_CAP, RECENTS_SECTION } from '../QuerySearch/constants';
import QuerySearch from '../QuerySearch/QuerySearch';
import { mockCodeMirrorDomApis } from './codemirrorDomMocks';
const CM_ROOT_SELECTOR = '.cm-editor';
const CM_EDITOR_SELECTOR = '.cm-editor .cm-content';
const TOOLTIP_SELECTOR = '.cm-tooltip-autocomplete';
const COMPLETION_LABEL_SELECTOR = '.cm-completionLabel';
const DELETE_BUTTON_SELECTOR = '.cm-recent-delete';
const FRONTEND_FILTER = "service.name = 'frontend'";
const STATUS_CODE_FILTER = "http.status_code = '500'";
const TRACES_FILTER = "name = 'HTTP GET'";
beforeAll(() => {
mockCodeMirrorDomApis();
});
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
useDashboardStore: (): { dashboardData: undefined } => ({
dashboardData: undefined,
}),
}));
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
getKeySuggestions: jest.fn().mockResolvedValue({
data: { data: { keys: {} } },
}),
}));
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
getValueSuggestions: jest.fn().mockResolvedValue({
data: { data: { values: { stringValues: [], numberValues: [] } } },
}),
}));
function renderLogsSearch(onChange: (value: string) => void = jest.fn()): void {
render(
<QuerySearch
onChange={onChange}
queryData={initialQueriesMap.logs.builder.queryData[0]}
dataSource={DataSource.LOGS}
/>,
);
}
function saveLogsRecent(expression: string): void {
recentQueriesStore.save({ signal: 'logs', filter: { expression } });
}
function getEditorView(): EditorView | null {
const root = document.querySelector<HTMLElement>(CM_ROOT_SELECTOR);
return root ? EditorView.findFromDOM(root) : null;
}
function getDocText(): string {
return getEditorView()?.state.doc.toString() ?? '';
}
function isCompletionOpen(): boolean {
const view = getEditorView();
return !!view && completionStatus(view.state) === 'active';
}
// Reads recents from completion state, not the tooltip: the tooltip is a later render
// pass over this same state, so going to the source drops a layer of timing.
function getRecentLabels(): string[] {
const view = getEditorView();
if (!view) {
return [];
}
return currentCompletions(view.state)
.filter((completion) => completion.section === RECENTS_SECTION)
.map((completion) => completion.label);
}
async function renderAndFocus(
onChange: (value: string) => void = jest.fn(),
): Promise<HTMLElement> {
renderLogsSearch(onChange);
const editor = await waitFor(
() => {
const element = document.querySelector(CM_EDITOR_SELECTOR);
expect(element).toBeInTheDocument();
return element as HTMLElement;
},
{ timeout: 2000 },
);
await userEvent.click(editor);
return editor;
}
function openRecents(): Promise<void> {
return waitFor(
() => {
const view = getEditorView();
if (view && !isCompletionOpen()) {
startCompletion(view);
}
expect(getRecentLabels().length).toBeGreaterThan(0);
},
{ timeout: 3000 },
);
}
describe('QuerySearch recent searches', () => {
beforeEach(() => {
recentQueriesStore.useRecentQueriesStore.setState({ buckets: {} });
localStorage.clear();
});
it('shows a saved recent query under "Recent searches" on focus', async () => {
saveLogsRecent(FRONTEND_FILTER);
await renderAndFocus();
await openRecents();
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual([FRONTEND_FILTER]);
},
{ timeout: 3000 },
);
const view = getEditorView() as EditorView;
const [recent] = currentCompletions(view.state);
expect(recent.section).toBe(RECENTS_SECTION);
});
it('filters recents by substring as the user types', async () => {
saveLogsRecent(FRONTEND_FILTER);
saveLogsRecent(STATUS_CODE_FILTER);
const editor = await renderAndFocus();
await openRecents();
await userEvent.type(editor, 'status_code');
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual([STATUS_CODE_FILTER]);
},
{ timeout: 3000 },
);
});
it('does not surface recents saved under a different signal', async () => {
recentQueriesStore.save({
signal: 'traces',
filter: { expression: TRACES_FILTER },
});
saveLogsRecent(FRONTEND_FILTER);
await renderAndFocus();
await openRecents();
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual([FRONTEND_FILTER]);
},
{ timeout: 3000 },
);
});
it('excludes a recent that exactly matches the current editor text', async () => {
const supersetFilter = `${FRONTEND_FILTER} AND ${STATUS_CODE_FILTER}`;
saveLogsRecent(FRONTEND_FILTER);
saveLogsRecent(supersetFilter);
const editor = await renderAndFocus();
await openRecents();
await userEvent.type(editor, FRONTEND_FILTER);
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual([supersetFilter]);
},
{ timeout: 3000 },
);
});
it('caps the dropdown at RECENTS_DISPLAY_CAP entries, newest first', async () => {
const filters = Array.from(
{ length: RECENTS_DISPLAY_CAP + 1 },
(_, index) => `attribute_${index + 1} = 'v'`,
);
filters.forEach((filter) => saveLogsRecent(filter));
const expectedLabels = [...filters].reverse().slice(0, RECENTS_DISPLAY_CAP);
await renderAndFocus();
await openRecents();
await waitFor(
() => {
expect(getRecentLabels()).toStrictEqual(expectedLabels);
},
{ timeout: 3000 },
);
});
it('applies the full expression to the editor when a recent is clicked', async () => {
saveLogsRecent(FRONTEND_FILTER);
const onChange = jest.fn();
await renderAndFocus(onChange);
await openRecents();
const option = await waitFor(
() => {
const node = Array.from(
document.querySelectorAll<HTMLElement>(COMPLETION_LABEL_SELECTOR),
).find((element) => element.textContent === FRONTEND_FILTER);
expect(node).toBeDefined();
return node as HTMLElement;
},
{ timeout: 3000 },
);
await userEvent.click(option);
await waitFor(
() => {
expect(getDocText()).toBe(FRONTEND_FILTER);
},
{ timeout: 2000 },
);
expect(onChange).toHaveBeenCalledWith(FRONTEND_FILTER);
await waitFor(
() => {
expect(document.querySelector(TOOLTIP_SELECTOR)).not.toBeInTheDocument();
},
{ timeout: 2000 },
);
});
it('removes a recent from the dropdown and the store when delete is clicked', async () => {
saveLogsRecent(FRONTEND_FILTER);
await renderAndFocus();
await openRecents();
const deleteButton = await waitFor(
() => {
const button = document.querySelector(DELETE_BUTTON_SELECTOR);
expect(button).toBeInTheDocument();
return button as HTMLElement;
},
{ timeout: 3000 },
);
// fireEvent: the button preventDefaults pointerdown, which makes userEvent.click drop the mouse chain.
fireEvent.click(deleteButton);
await waitFor(
() => {
expect(recentQueriesStore.list('logs')).toHaveLength(0);
expect(getRecentLabels()).not.toContain(FRONTEND_FILTER);
},
{ timeout: 2000 },
);
expect(getDocText()).toBe('');
});
});

View File

@@ -41,7 +41,6 @@ export const QUERY_BUILDER_FUNCTIONS = {
HASANY: 'hasAny',
HASALL: 'hasAll',
HASTOKEN: 'hasToken',
SEARCH: 'search',
};
export function negateOperator(operatorOrFunction: string): string {

View File

@@ -7,6 +7,7 @@ export enum FeatureKeys {
GATEWAY = 'gateway',
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
USE_JSON_BODY = 'use_json_body',
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',

View File

@@ -437,17 +437,6 @@ describe('Create Alert Channel', () => {
render(<CreateAlertChannels preType={ChannelType.GoogleChat} />);
});
// paste instead of type: a per-keystroke re-render of the whole form
// pushes these tests past the 5s jest timeout on slower CI runners
async function fillField(
user: ReturnType<typeof userEvent.setup>,
testId: string,
value: string,
): Promise<void> {
await user.click(screen.getByTestId(testId));
await user.paste(value);
}
it('Should check if the selected item in the type dropdown has text "Google Chat"', () => {
expect(screen.getByText('Google Chat')).toBeInTheDocument();
});
@@ -474,8 +463,14 @@ describe('Create Alert Channel', () => {
it('Should check if saving with a webhook url outside chat.googleapis.com displays error notification', async () => {
const user = userEvent.setup();
await fillField(user, 'channel-name-textbox', 'gchat-channel');
await fillField(user, 'webhook-url-textbox', 'https://example.com/webhook');
await user.type(
screen.getByTestId('channel-name-textbox'),
'gchat-channel',
);
await user.type(
screen.getByTestId('webhook-url-textbox'),
'https://example.com/webhook',
);
await user.click(screen.getByTestId('save-channel-button'));
@@ -501,8 +496,11 @@ describe('Create Alert Channel', () => {
const user = userEvent.setup();
await fillField(user, 'channel-name-textbox', 'gchat-channel');
await fillField(user, 'webhook-url-textbox', validWebhookUrl);
await user.type(
screen.getByTestId('channel-name-textbox'),
'gchat-channel',
);
await user.type(screen.getByTestId('webhook-url-textbox'), validWebhookUrl);
await user.click(screen.getByTestId('save-channel-button'));

View File

@@ -37,6 +37,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { Exception, PayloadProps } from 'types/api/errors/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
import { FilterDropdownExtendsProps } from './types';
import {
extractFilterValues,
@@ -416,6 +418,11 @@ function AllErrors(): JSX.Element {
},
];
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
(
paginations: TablePaginationConfig,
@@ -451,7 +458,7 @@ function AllErrors(): JSX.Element {
useEffect(() => {
if (!isUndefined(errorCountResponse.data?.payload)) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
logEvent('Exception: List page visited', {

View File

@@ -130,28 +130,6 @@ describe('Footer utils', () => {
};
expect(validateCreateAlertState(currentArgs)).toBeNull();
});
it('when threshold channels are null', () => {
const currentArgs: BuildCreateAlertRulePayloadArgs = {
...args,
basicAlertState: {
...args.basicAlertState,
name: 'test name',
},
thresholdState: {
...args.thresholdState,
thresholds: [
{
...args.thresholdState.thresholds[0],
channels: null as unknown as string[],
},
],
},
};
expect(validateCreateAlertState(currentArgs)).toBe(
'Please select at least one channel for each threshold or enable routing policies',
);
});
});
describe('getNotificationSettingsProps', () => {

View File

@@ -44,8 +44,7 @@ export function validateCreateAlertState(
if (!threshold.label) {
return 'Please enter a label for each threshold';
}
// this runs during render, so a throw here takes down the whole page
if (!notificationSettings.routingPolicies && !threshold.channels?.length) {
if (!notificationSettings.routingPolicies && !threshold.channels.length) {
return 'Please select at least one channel for each threshold or enable routing policies';
}
}

View File

@@ -316,34 +316,6 @@ describe('CreateAlertV2 utils', () => {
});
});
describe('getThresholdStateFromAlertDef null channels', () => {
it('falls back to an empty array so downstream consumers never see null', () => {
const def: PostableAlertRuleV2 = {
...defaultPostableAlertRuleV2,
condition: {
...defaultPostableAlertRuleV2.condition,
thresholds: {
kind: 'basic',
spec: [
{
name: 'critical',
target: 1,
targetUnit: UniversalYAxisUnit.MINUTES,
channels: null as unknown as string[],
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
op: AlertThresholdOperator.IS_ABOVE,
},
],
},
},
};
expect(
getThresholdStateFromAlertDef(def).thresholds[0].channels,
).toStrictEqual([]);
});
});
describe('normalizeOperator', () => {
it.each([
['1', AlertThresholdOperator.IS_ABOVE],

View File

@@ -258,9 +258,7 @@ export function getThresholdStateFromAlertDef(
recoveryThresholdValue: null,
unit: threshold.targetUnit,
color: getColorForThreshold(threshold.name),
// rules created outside the UI can come back with a null channels
// field; drop the guard once the API enforces the schema
channels: threshold.channels ?? [],
channels: threshold.channels,
})) || [],
selectedQuery: alertDef.condition.selectedQueryName || '',
operator:

View File

@@ -124,9 +124,7 @@ export function buildBaseConfig({
side: 2,
isDarkMode,
isLogScale,
// Graph and bar plot time on X; every other panel type here does not.
isTimeAxis:
panelType === PANEL_TYPES.TIME_SERIES || panelType === PANEL_TYPES.BAR,
panelType,
});
builder.addAxis({
@@ -136,6 +134,7 @@ export function buildBaseConfig({
isDarkMode,
isLogScale,
yAxisUnit,
panelType,
});
return builder;

View File

@@ -35,6 +35,7 @@ import { openInNewTab } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
import { FeatureKeys } from '../../../constants/features';
import { DOCS_LINKS } from '../constants';
import { columns, TIME_PICKER_OPTIONS } from './constants';
@@ -211,13 +212,19 @@ function ServiceMetrics({
const topLevelOperations = useMemo(() => Object.entries(data || {}), [data]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryRangeRequestData = useMemo(
() =>
getQueryRangeRequestData({
topLevelOperations,
globalSelectedInterval,
dotMetricsEnabled,
}),
[globalSelectedInterval, topLevelOperations],
[globalSelectedInterval, topLevelOperations, dotMetricsEnabled],
);
const dataQueries = useGetQueriesRange(

View File

@@ -562,9 +562,13 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 1`,
},
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
op: '=',
value: 1,
},
],
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
limit: null,
orderBy: [],
@@ -644,9 +648,13 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 0`,
},
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
op: '=',
value: 0,
},
],
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
limit: null,
orderBy: [],

View File

@@ -121,6 +121,12 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
plan_version: 'test-plan-version',
},
},
featureFlags: [
{
name: 'DOT_METRICS_ENABLED',
active: false,
},
],
} as any);
const mockEntity = {

View File

@@ -1,4 +1,5 @@
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
import {
@@ -75,7 +76,7 @@ export function buildEntityMetricsChartConfig({
show: true,
side: 2,
isDarkMode,
isTimeAxis: true,
panelType: PANEL_TYPES.TIME_SERIES,
});
builder.addAxis({
@@ -84,6 +85,7 @@ export function buildEntityMetricsChartConfig({
side: 3,
isDarkMode,
yAxisUnit,
panelType: PANEL_TYPES.TIME_SERIES,
});
if (!apiResponse?.data?.result) {

View File

@@ -1208,9 +1208,13 @@ export const getNamespaceMetricsQueryPayload = (
type: 'tag',
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED}) > 0`,
},
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
op: '>',
value: 0,
},
],
legend: 'desired',
limit: null,
orderBy: [],
@@ -1257,9 +1261,13 @@ export const getNamespaceMetricsQueryPayload = (
type: 'tag',
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_AVAILABLE}) > 0`,
},
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
op: '>',
value: 0,
},
],
legend: 'available',
limit: null,
orderBy: [],
@@ -1617,13 +1625,13 @@ export const getNamespaceMetricsQueryPayload = (
reduceTo: ReduceOperators.LAST,
spaceAggregation: 'max',
stepInterval: 60,
timeAggregation: 'latest',
timeAggregation: 'avg',
},
],
queryFormulas: [
{
disabled: false,
expression: '(B/A) * 100',
expression: 'A/B',
legend: 'util %',
queryName: 'F1',
},

View File

@@ -17,6 +17,8 @@ import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
getHostQueryPayload,
getNodeQueryPayload,
@@ -51,12 +53,23 @@ function NodeMetrics({
};
}, [timestamp]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryPayloads = useMemo(() => {
if (nodeName) {
return getNodeQueryPayload(clusterName, nodeName, start, end);
return getNodeQueryPayload(
clusterName,
nodeName,
start,
end,
dotMetricsEnabled,
);
}
return getHostQueryPayload(hostName, start, end);
}, [nodeName, hostName, clusterName, start, end]);
return getHostQueryPayload(hostName, start, end, dotMetricsEnabled);
}, [nodeName, hostName, clusterName, start, end, dotMetricsEnabled]);
const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo;
const queries = useQueries(

View File

@@ -12,11 +12,13 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { FeatureKeys } from '../../../constants/features';
import { getPodQueryPayload, podWidgetInfo } from './constants';
function PodMetrics({
@@ -52,9 +54,14 @@ function PodMetrics({
scrollLeft: 0,
});
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryPayloads = useMemo(
() => getPodQueryPayload(clusterName, podName, start, end),
[clusterName, end, podName, start],
() => getPodQueryPayload(clusterName, podName, start, end, dotMetricsEnabled),
[clusterName, end, podName, start, dotMetricsEnabled],
);
const queries = useQueries(
queryPayloads.map((payload) => ({

View File

@@ -1,39 +1,56 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import type { Having } from 'types/api/queryBuilder/queryBuilderData';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { Having as HavingV5 } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
const buildSumGreaterThanZeroHaving = (
metricKey: string,
useV5HavingFormat: boolean,
): Having[] | HavingV5 =>
useV5HavingFormat
? { expression: `sum(${metricKey}) > 0` }
: [{ columnName: `SUM(${metricKey})`, op: '>', value: 0 }];
export const getPodQueryPayload = (
clusterName: string,
podName: string,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sClusterNameKey = 'k8s.cluster.name';
const k8sPodNameKey = 'k8s.pod.name';
const containerCpuUtilKey = 'container.cpu.usage';
const containerMemUsageKey = 'container.memory.usage';
const k8sContainerCpuReqKey = 'k8s.container.cpu_request';
const k8sContainerCpuLimitKey = 'k8s.container.cpu_limit';
const k8sContainerMemReqKey = 'k8s.container.memory_request';
const k8sContainerMemLimitKey = 'k8s.container.memory_limit';
const k8sPodFsAvailKey = 'k8s.pod.filesystem.available';
const k8sPodFsCapKey = 'k8s.pod.filesystem.capacity';
const k8sPodNetIoKey = 'k8s.pod.network.io';
const podLegendTemplate = '{{k8s.pod.name}}';
const podLegendUsage = 'usage - {{k8s.pod.name}}';
const podLegendLimit = 'limit - {{k8s.pod.name}}';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
const containerCpuUtilKey = dotMetricsEnabled
? 'container.cpu.usage'
: 'container_cpu_usage';
const containerMemUsageKey = dotMetricsEnabled
? 'container.memory.usage'
: 'container_memory_usage';
const k8sContainerCpuReqKey = dotMetricsEnabled
? 'k8s.container.cpu_request'
: 'k8s_container_cpu_request';
const k8sContainerCpuLimitKey = dotMetricsEnabled
? 'k8s.container.cpu_limit'
: 'k8s_container_cpu_limit';
const k8sContainerMemReqKey = dotMetricsEnabled
? 'k8s.container.memory_request'
: 'k8s_container_memory_request';
const k8sContainerMemLimitKey = dotMetricsEnabled
? 'k8s.container.memory_limit'
: 'k8s_container_memory_limit';
const k8sPodFsAvailKey = dotMetricsEnabled
? 'k8s.pod.filesystem.available'
: 'k8s_pod_filesystem_available';
const k8sPodFsCapKey = dotMetricsEnabled
? 'k8s.pod.filesystem.capacity'
: 'k8s_pod_filesystem_capacity';
const k8sPodNetIoKey = dotMetricsEnabled
? 'k8s.pod.network.io'
: 'k8s_pod_network_io';
const podLegendTemplate = dotMetricsEnabled
? '{{k8s.pod.name}}'
: '{{k8s_pod_name}}';
const podLegendUsage = dotMetricsEnabled
? 'usage - {{k8s.pod.name}}'
: 'usage - {{k8s_pod_name}}';
const podLegendLimit = dotMetricsEnabled
? 'limit - {{k8s.pod.name}}'
: 'limit - {{k8s_pod_name}}';
return [
{
@@ -1010,17 +1027,36 @@ export const getNodeQueryPayload = (
nodeName: string,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sClusterNameKey = 'k8s.cluster.name';
const k8sNodeNameKey = 'k8s.node.name';
const k8sNodeCpuTimeKey = 'k8s.node.cpu.time';
const k8sNodeAllocCpuKey = 'k8s.node.allocatable_cpu';
const k8sNodeMemWsKey = 'k8s.node.memory.working_set';
const k8sNodeAllocMemKey = 'k8s.node.allocatable_memory';
const k8sNodeNetIoKey = 'k8s.node.network.io';
const k8sNodeFsAvailKey = 'k8s.node.filesystem.available';
const k8sNodeFsCapKey = 'k8s.node.filesystem.capacity';
const podLegend = '{{k8s.node.name}}';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const k8sNodeNameKey = dotMetricsEnabled ? 'k8s.node.name' : 'k8s_node_name';
const k8sNodeCpuTimeKey = dotMetricsEnabled
? 'k8s.node.cpu.time'
: 'k8s_node_cpu_time';
const k8sNodeAllocCpuKey = dotMetricsEnabled
? 'k8s.node.allocatable_cpu'
: 'k8s_node_allocatable_cpu';
const k8sNodeMemWsKey = dotMetricsEnabled
? 'k8s.node.memory.working_set'
: 'k8s_node_memory_working_set';
const k8sNodeAllocMemKey = dotMetricsEnabled
? 'k8s.node.allocatable_memory'
: 'k8s_node_allocatable_memory';
const k8sNodeNetIoKey = dotMetricsEnabled
? 'k8s.node.network.io'
: 'k8s_node_network_io';
const k8sNodeFsAvailKey = dotMetricsEnabled
? 'k8s.node.filesystem.available'
: 'k8s_node_filesystem_available';
const k8sNodeFsCapKey = dotMetricsEnabled
? 'k8s.node.filesystem.capacity'
: 'k8s_node_filesystem_capacity';
const podLegend = dotMetricsEnabled
? '{{k8s.node.name}}'
: '{{k8s_node_name}}';
return [
{
@@ -1550,24 +1586,48 @@ export const getHostQueryPayload = (
hostName: string,
start: number,
end: number,
useV5HavingFormat = false,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const hostNameKey = 'host.name';
const cpuTimeKey = 'system.cpu.time';
const memUsageKey = 'system.memory.usage';
const load1mKey = 'system.cpu.load_average.1m';
const load5mKey = 'system.cpu.load_average.5m';
const load15mKey = 'system.cpu.load_average.15m';
const netIoKey = 'system.network.io';
const netPktsKey = 'system.network.packets';
const netErrKey = 'system.network.errors';
const netDropKey = 'system.network.dropped';
const netConnKey = 'system.network.connections';
const diskIoKey = 'system.disk.io';
const diskOpTimeKey = 'system.disk.operation_time';
const diskOpsKey = 'system.disk.operations';
const diskPendingKey = 'system.disk.pending_operations';
const fsUsageKey = 'system.filesystem.usage';
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
const cpuTimeKey = dotMetricsEnabled ? 'system.cpu.time' : 'system_cpu_time';
const memUsageKey = dotMetricsEnabled
? 'system.memory.usage'
: 'system_memory_usage';
const load1mKey = dotMetricsEnabled
? 'system.cpu.load_average.1m'
: 'system_cpu_load_average_1m';
const load5mKey = dotMetricsEnabled
? 'system.cpu.load_average.5m'
: 'system_cpu_load_average_5m';
const load15mKey = dotMetricsEnabled
? 'system.cpu.load_average.15m'
: 'system_cpu_load_average_15m';
const netIoKey = dotMetricsEnabled ? 'system.network.io' : 'system_network_io';
const netPktsKey = dotMetricsEnabled
? 'system.network.packets'
: 'system_network_packets';
const netErrKey = dotMetricsEnabled
? 'system.network.errors'
: 'system_network_errors';
const netDropKey = dotMetricsEnabled
? 'system.network.dropped'
: 'system_network_dropped';
const netConnKey = dotMetricsEnabled
? 'system.network.connections'
: 'system_network_connections';
const diskIoKey = dotMetricsEnabled ? 'system.disk.io' : 'system_disk_io';
const diskOpTimeKey = dotMetricsEnabled
? 'system.disk.operation_time'
: 'system_disk_operation_time';
const diskOpsKey = dotMetricsEnabled
? 'system.disk.operations'
: 'system_disk_operations';
const diskPendingKey = dotMetricsEnabled
? 'system.disk.pending_operations'
: 'system_disk_pending_operations';
const fsUsageKey = dotMetricsEnabled
? 'system.filesystem.usage'
: 'system_filesystem_usage';
return [
{
@@ -1813,7 +1873,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
@@ -1862,7 +1928,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
@@ -2088,7 +2160,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(netIoKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${netIoKey})`,
op: '>',
value: 0,
},
],
legend: '{{device}}::{{direction}}',
limit: 30,
orderBy: [],
@@ -2544,7 +2622,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(diskOpsKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${diskOpsKey})`,
op: '>',
value: 0,
},
],
legend: '{{device}}::{{direction}}',
limit: null,
orderBy: [],
@@ -2613,7 +2697,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(diskPendingKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${diskPendingKey})`,
op: '>',
value: 0,
},
],
legend: '{{device}}',
limit: null,
orderBy: [],
@@ -2689,7 +2779,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(diskOpTimeKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${diskOpTimeKey})`,
op: '>',
value: 0,
},
],
legend: '{{device}}::{{direction}}',
limit: null,
orderBy: [],

View File

@@ -1,4 +1,5 @@
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getInitialStackedBands } from 'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils';
import { getLegend } from 'lib/dashboard/getQueryResults';
import getLabelName from 'lib/getLabelName';
@@ -72,7 +73,7 @@ export function buildMeterChartConfig({
show: true,
side: 2,
isDarkMode,
isTimeAxis: true,
panelType: PANEL_TYPES.BAR,
});
builder.addAxis({
@@ -81,6 +82,7 @@ export function buildMeterChartConfig({
side: 3,
isDarkMode,
yAxisUnit,
panelType: PANEL_TYPES.BAR,
});
if (!apiResponse?.data?.result) {

View File

@@ -21,6 +21,7 @@ export const databaseCallsRPS = ({
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}: DatabaseCallsRPSProps): QueryBuilderData => {
const autocompleteData: BaseAutocompleteData[] = [
{
@@ -32,7 +33,7 @@ export const databaseCallsRPS = ({
const groupBy: BaseAutocompleteData[] = [
{
dataType: DataTypes.String,
key: WidgetKeys.DbSystem,
key: dotMetricsEnabled ? WidgetKeys.Db_system : WidgetKeys.Db_system_norm,
type: 'tag',
},
];
@@ -41,7 +42,9 @@ export const databaseCallsRPS = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -72,6 +75,7 @@ export const databaseCallsRPS = ({
export const databaseCallsAvgDuration = ({
servicename,
tagFilterItems,
dotMetricsEnabled,
}: DatabaseCallProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
key: WidgetKeys.SignozDbLatencySum,
@@ -88,7 +92,9 @@ export const databaseCallsAvgDuration = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},

View File

@@ -32,6 +32,7 @@ export const externalCallErrorPercent = ({
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
key: WidgetKeys.SignozExternalCallLatencyCount,
@@ -48,7 +49,9 @@ export const externalCallErrorPercent = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -58,7 +61,7 @@ export const externalCallErrorPercent = ({
{
id: '',
key: {
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
dataType: DataTypes.Int64,
type: MetricsType.Tag,
},
@@ -71,7 +74,9 @@ export const externalCallErrorPercent = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -115,6 +120,7 @@ export const externalCallErrorPercent = ({
export const externalCallDuration = ({
servicename,
tagFilterItems,
dotMetricsEnabled,
}: ExternalCallProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
dataType: DataTypes.Float64,
@@ -135,7 +141,9 @@ export const externalCallDuration = ({
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -175,6 +183,7 @@ export const externalCallRpsByAddress = ({
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
const autocompleteData: BaseAutocompleteData[] = [
{
@@ -189,7 +198,9 @@ export const externalCallRpsByAddress = ({
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -220,6 +231,7 @@ export const externalCallDurationByAddress = ({
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
dataType: DataTypes.Float64,
@@ -239,7 +251,9 @@ export const externalCallDurationByAddress = ({
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,

View File

@@ -37,10 +37,15 @@ export const latency = ({
tagFilterItems,
isSpanMetricEnable = false,
topLevelOperationsRoute,
dotMetricsEnabled,
}: LatencyProps): QueryBuilderData => {
const signozLatencyBucketMetrics = WidgetKeys.SignozLatencyBucket;
const signozLatencyBucketMetrics = dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm;
const signozMetricsServiceName = WidgetKeys.OTelServiceName;
const signozMetricsServiceName = dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm;
const newAutoCompleteData: BaseAutocompleteData = {
key: isSpanMetricEnable
? signozLatencyBucketMetrics
@@ -282,21 +287,28 @@ export const apDexMetricsQueryBuilderQueries = ({
threashold,
delta,
metricsBuckets,
dotMetricsEnabled,
}: ApDexMetricsQueryBuilderQueriesProps): QueryBuilderData => {
const autoCompleteDataA: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyCount,
key: dotMetricsEnabled
? WidgetKeys.SignozLatencyCount
: WidgetKeys.SignozLatencyCountNorm,
dataType: DataTypes.Float64,
type: '',
};
const autoCompleteDataB: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyBucket,
key: dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm,
dataType: DataTypes.Float64,
type: '',
};
const autoCompleteDataC: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyBucket,
key: dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm,
dataType: DataTypes.Float64,
type: '',
};
@@ -305,7 +317,9 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -329,7 +343,7 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -349,7 +363,9 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -383,7 +399,7 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -393,7 +409,9 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -456,10 +474,13 @@ export const operationPerSec = ({
servicename,
tagFilterItems,
topLevelOperations,
dotMetricsEnabled,
}: OperationPerSecProps): QueryBuilderData => {
const autocompleteData: BaseAutocompleteData[] = [
{
key: WidgetKeys.SignozLatencyCount,
key: dotMetricsEnabled
? WidgetKeys.SignozLatencyCount
: WidgetKeys.SignozLatencyCountNorm,
dataType: DataTypes.Float64,
type: '',
},
@@ -470,7 +491,9 @@ export const operationPerSec = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -511,6 +534,7 @@ export const errorPercentage = ({
servicename,
tagFilterItems,
topLevelOperations,
dotMetricsEnabled,
}: OperationPerSecProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
key: WidgetKeys.SignozCallsTotal,
@@ -529,7 +553,9 @@ export const errorPercentage = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -549,7 +575,7 @@ export const errorPercentage = ({
{
id: '',
key: {
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
dataType: DataTypes.Int64,
type: MetricsType.Tag,
},
@@ -563,7 +589,9 @@ export const errorPercentage = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},

View File

@@ -21,9 +21,12 @@ import { getQueryBuilderQuerieswithFormula } from './MetricsPageQueriesFactory';
export const topOperationQueries = ({
servicename,
dotMetricsEnabled,
}: TopOperationQueryFactoryProps): QueryBuilderData => {
const latencyAutoCompleteData: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyBucket,
key: dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm,
dataType: DataTypes.Float64,
type: '',
};
@@ -35,7 +38,9 @@ export const topOperationQueries = ({
};
const numOfCallAutoCompleteData: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyCount,
key: dotMetricsEnabled
? WidgetKeys.SignozLatencyCount
: WidgetKeys.SignozLatencyCountNorm,
dataType: DataTypes.Float64,
type: '',
};
@@ -44,7 +49,9 @@ export const topOperationQueries = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -58,7 +65,9 @@ export const topOperationQueries = ({
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -68,7 +77,7 @@ export const topOperationQueries = ({
id: '',
key: {
dataType: DataTypes.Int64,
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
type: MetricsType.Tag,
},
op: OPERATORS.IN,

View File

@@ -28,6 +28,8 @@ import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
GraphTitle,
MENU_ITEMS,
@@ -87,7 +89,12 @@ function DBCall(): JSX.Element {
[queries],
);
const legend = '{{db.system}}';
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const legend = dotMetricsEnabled ? '{{db.system}}' : '{{db_system}}';
const databaseCallsRPSWidget = useMemo(
() =>
@@ -99,6 +106,7 @@ function DBCall(): JSX.Element {
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -109,7 +117,7 @@ function DBCall(): JSX.Element {
id: SERVICE_CHART_ID.dbCallsRPS,
fillSpans: false,
}),
[servicename, tagFilterItems, legend],
[servicename, tagFilterItems, dotMetricsEnabled, legend],
);
const databaseCallsAverageDurationWidget = useMemo(
() =>
@@ -120,6 +128,7 @@ function DBCall(): JSX.Element {
builder: databaseCallsAvgDuration({
servicename,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -130,7 +139,7 @@ function DBCall(): JSX.Element {
id: GraphTitle.DATABASE_CALLS_AVG_DURATION,
fillSpans: true,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const stepInterval = useMemo(
@@ -148,7 +157,7 @@ function DBCall(): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
logEvent('APM: Service detail page visited', {

View File

@@ -30,6 +30,8 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
GraphTitle,
legend,
@@ -82,6 +84,10 @@ function External(): JSX.Element {
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
[queries],
);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const externalCallErrorWidget = useMemo(
() =>
@@ -93,6 +99,7 @@ function External(): JSX.Element {
servicename,
legend: legend.address,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -102,7 +109,7 @@ function External(): JSX.Element {
yAxisUnit: '%',
id: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const selectedTraceTags = useMemo(
@@ -119,6 +126,7 @@ function External(): JSX.Element {
builder: externalCallDuration({
servicename,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -129,7 +137,7 @@ function External(): JSX.Element {
id: GraphTitle.EXTERNAL_CALL_DURATION,
fillSpans: true,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const errorApmToTraceQuery = useGetAPMToTracesQueries({
@@ -163,7 +171,7 @@ function External(): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
logEvent('APM: Service detail page visited', {
@@ -186,6 +194,7 @@ function External(): JSX.Element {
servicename,
legend: legend.address,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -196,7 +205,7 @@ function External(): JSX.Element {
id: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
fillSpans: true,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const externalCallDurationAddressWidget = useMemo(
@@ -209,6 +218,7 @@ function External(): JSX.Element {
servicename,
legend: legend.address,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -219,7 +229,7 @@ function External(): JSX.Element {
id: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
fillSpans: true,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const apmToTraceQuery = useGetAPMToTracesQueries({

View File

@@ -93,12 +93,15 @@ function Application(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
[handleSetTimeStamp],
);
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
logEvent('APM: Service detail page visited', {
@@ -156,6 +159,7 @@ function Application(): JSX.Element {
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -165,7 +169,7 @@ function Application(): JSX.Element {
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
[servicename, tagFilterItems, topLevelOperationsRoute, dotMetricsEnabled],
);
const errorPercentageWidget = useMemo(
@@ -178,6 +182,7 @@ function Application(): JSX.Element {
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -188,7 +193,7 @@ function Application(): JSX.Element {
id: SERVICE_CHART_ID.errorPercentage,
fillSpans: true,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
[servicename, tagFilterItems, topLevelOperationsRoute, dotMetricsEnabled],
);
const stepInterval = useMemo(

View File

@@ -22,6 +22,8 @@ import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/Me
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../../../constants/features';
import { useAppContext } from '../../../../../providers/App/App';
import { IServiceName } from '../../types';
import { ApDexMetricsProps } from './types';
@@ -36,6 +38,10 @@ function ApDexMetrics({
}: ApDexMetricsProps): JSX.Element {
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const apDexMetricsWidget = useMemo(
() =>
getWidgetQueryBuilder({
@@ -49,6 +55,7 @@ function ApDexMetrics({
threashold: thresholdValue || 0,
delta: delta || false,
metricsBuckets: metricsBuckets || [],
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -74,6 +81,7 @@ function ApDexMetrics({
tagFilterItems,
thresholdValue,
topLevelOperationsRoute,
dotMetricsEnabled,
],
);

View File

@@ -3,6 +3,8 @@ import Spinner from 'components/Spinner';
import { useGetMetricMeta } from 'hooks/apDex/useGetMetricMeta';
import useErrorNotification from 'hooks/useErrorNotification';
import { FeatureKeys } from '../../../../../constants/features';
import { useAppContext } from '../../../../../providers/App/App';
import { WidgetKeys } from '../../../constant';
import { IServiceName } from '../../types';
import ApDexMetrics from './ApDexMetrics';
@@ -18,8 +20,17 @@ function ApDexMetricsApplication({
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const signozLatencyBucketMetrics = dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm;
const { data, isLoading, error } = useGetMetricMeta(
WidgetKeys.SignozLatencyBucket,
signozLatencyBucketMetrics,
servicename,
);
useErrorNotification(error);

View File

@@ -56,6 +56,10 @@ function ServiceOverview({
[isSpanMetricEnable, queries],
);
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const latencyWidget = useMemo(
() =>
getWidgetQueryBuilder({
@@ -67,6 +71,7 @@ function ServiceOverview({
tagFilterItems,
isSpanMetricEnable,
topLevelOperationsRoute,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -76,7 +81,13 @@ function ServiceOverview({
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
}),
[isSpanMetricEnable, servicename, tagFilterItems, topLevelOperationsRoute],
[
isSpanMetricEnable,
servicename,
tagFilterItems,
topLevelOperationsRoute,
dotMetricsEnabled,
],
);
const isQueryEnabled =

View File

@@ -19,6 +19,8 @@ import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../../constants/features';
import { useAppContext } from '../../../../providers/App/App';
import { IServiceName } from '../types';
import { title } from './config';
import ColumnWithLink from './TableRenderer/ColumnWithLink';
@@ -42,6 +44,11 @@ function TopOperationMetrics(): JSX.Element {
convertRawQueriesToTraceSelectedTags(queries) || [],
);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const keyOperationWidget = useMemo(
() =>
getWidgetQueryBuilder({
@@ -50,13 +57,14 @@ function TopOperationMetrics(): JSX.Element {
promql: [],
builder: topOperationQueries({
servicename,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
},
panelTypes: PANEL_TYPES.TABLE,
}),
[servicename],
[servicename, dotMetricsEnabled],
);
const updatedQuery = updateStepInterval(keyOperationWidget.query);

View File

@@ -10,6 +10,7 @@ export interface IServiceName {
export interface TopOperationQueryFactoryProps {
servicename: IServiceName['servicename'];
dotMetricsEnabled: boolean;
}
export interface ExternalCallDurationByAddressProps extends ExternalCallProps {
@@ -19,6 +20,7 @@ export interface ExternalCallDurationByAddressProps extends ExternalCallProps {
export interface ExternalCallProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
dotMetricsEnabled: boolean;
}
export interface BuilderQueriesProps {
@@ -50,6 +52,7 @@ export interface OperationPerSecProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
topLevelOperations: string[];
dotMetricsEnabled: boolean;
}
export interface LatencyProps {
@@ -57,6 +60,7 @@ export interface LatencyProps {
tagFilterItems: TagFilterItem[];
isSpanMetricEnable?: boolean;
topLevelOperationsRoute: string[];
dotMetricsEnabled: boolean;
}
export interface ApDexProps {
@@ -74,4 +78,5 @@ export interface TableRendererProps {
export interface ApDexMetricsQueryBuilderQueriesProps extends ApDexProps {
delta: boolean;
metricsBuckets: number[];
dotMetricsEnabled: boolean;
}

View File

@@ -85,11 +85,14 @@ export enum WidgetKeys {
HasError = 'hasError',
Address = 'address',
DurationNano = 'durationNano',
StatusCodeNorm = 'status_code',
StatusCode = 'status.code',
Operation = 'operation',
OperationName = 'operationName',
OTelServiceName = 'service.name',
Service_name_norm = 'service_name',
Service_name = 'service.name',
ServiceName = 'serviceName',
SignozLatencyCountNorm = 'signoz_latency_count',
SignozLatencyCount = 'signoz_latency.count',
SignozDBLatencyCount = 'signoz_db_latency_count',
DatabaseCallCount = 'signoz_database_call_count',
@@ -98,8 +101,10 @@ export enum WidgetKeys {
SignozCallsTotal = 'signoz_calls_total',
SignozExternalCallLatencyCount = 'signoz_external_call_latency_count',
SignozExternalCallLatencySum = 'signoz_external_call_latency_sum',
SignozLatencyBucket = 'signoz_latency.bucket',
DbSystem = 'db.system',
Signoz_latency_bucket_norm = 'signoz_latency_bucket',
Signoz_latency_bucket = 'signoz_latency.bucket',
Db_system = 'db.system',
Db_system_norm = 'db_system',
}
export const topOperationMetricsDownloadOptions: DownloadOptions = {

View File

@@ -32,4 +32,5 @@ export interface DatabaseCallsRPSProps extends DatabaseCallProps {
export interface DatabaseCallProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
dotMetricsEnabled: boolean;
}

View File

@@ -1521,9 +1521,9 @@ const onboardingConfigWithLinks = [
},
{
dataSource: 'nginx-tracing',
label: 'Nginx - OpenTelemetry',
label: 'Nginx - Tracing',
imgUrl: nginxUrl,
tags: ['apm/traces', 'logs', 'metrics'],
tags: ['apm/traces'],
module: 'apm',
relatedSearchKeywords: [
'apm',
@@ -1626,7 +1626,7 @@ const onboardingConfigWithLinks = [
dataSource: 'cloudflare-workers',
label: 'Cloudflare Workers',
imgUrl: cloudflareUrl,
tags: ['apm/traces', 'logs'],
tags: ['apm/traces'],
module: 'apm',
relatedSearchKeywords: [
'cloudflare',
@@ -5346,17 +5346,13 @@ const onboardingConfigWithLinks = [
dataSource: 'temporal',
label: 'Temporal',
imgUrl: temporalUrl,
tags: ['apm/traces', 'logs', 'metrics'],
tags: ['apm/traces'],
module: 'apm',
relatedSearchKeywords: [
'apm',
'application performance monitoring',
'integrations',
'logs',
'metrics',
'temporal',
'temporal logs',
'temporal metrics',
'temporal traces',
'traces',
'tracing',
@@ -5482,7 +5478,7 @@ const onboardingConfigWithLinks = [
dataSource: 'dbos',
label: 'DBOS',
imgUrl: dbosUrl,
tags: ['apm/traces', 'logs'],
tags: ['apm/traces'],
module: 'apm',
relatedSearchKeywords: [
'database oriented',
@@ -6626,7 +6622,7 @@ const onboardingConfigWithLinks = [
dataSource: 'opentelemetry-ebpf',
label: 'OpenTelemetry eBPF (OBI)',
imgUrl: opentelemetryUrl,
tags: ['apm/traces', 'metrics'],
tags: ['apm/traces'],
module: 'apm',
relatedSearchKeywords: [
'auto instrumentation',

View File

@@ -387,42 +387,4 @@ describe('useOptionsMenu', () => {
expect(remaining).toHaveLength(seedColumns.length);
});
});
describe('fieldsSelector.value drops legacy columns without a name', () => {
it('excludes entries missing name while keeping valid columns', () => {
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: { data: { data: { keys: {} } } },
isFetching: false,
});
(usePreferenceContext as jest.Mock).mockReturnValue({
traces: {
preferences: {
columns: [
{ name: 'body', fieldContext: 'log' },
{ key: 'legacy-key-no-name', fieldContext: 'log' },
{ name: 'timestamp', fieldContext: 'log' },
],
formatting: { format: 'table', maxLines: 1, fontSize: 'small' },
},
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
logs: {
preferences: { columns: [], formatting: {} },
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
}),
);
const fields = result.current.config.fieldsSelector?.value ?? [];
expect(fields.map((f) => f.name)).toStrictEqual(['body', 'timestamp']);
});
});
});

View File

@@ -399,7 +399,7 @@ const useOptionsMenu = ({
onReorder: reorderSelectColumns,
},
fieldsSelector: {
value: preferences?.columns?.filter((item) => has(item, 'name')) ?? [],
value: preferences?.columns ?? [],
onFieldsChange: updateColumns,
},
format: {

View File

@@ -53,6 +53,8 @@ import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../../constants/features';
import { useAppContext } from '../../../../providers/App/App';
import { selectStyle } from './config';
import { PLACEHOLDER } from './constant';
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
@@ -102,6 +104,11 @@ function QueryBuilderSearch({
const [isEditingTag, setIsEditingTag] = useState(false);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const {
updateTag,
handleClearTag,
@@ -121,6 +128,7 @@ function QueryBuilderSearch({
exampleQueries,
} = useAutoComplete(
query,
dotMetricsEnabled,
whereClauseConfig,
isLogsExplorerPage,
isInfraMonitoring,
@@ -138,6 +146,7 @@ function QueryBuilderSearch({
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
searchValue,
query,
dotMetricsEnabled,
searchKey,
isLogsExplorerPage,
isInfraMonitoring,

View File

@@ -14,6 +14,8 @@ import { SelectOption } from 'types/common/select';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
import QueryChip from './components/QueryChip';
import { QueryChipItem, SearchContainer } from './styles';
@@ -40,7 +42,12 @@ function ResourceAttributesFilter({
SelectOption<string, string>[]
>([]);
const resourceDeploymentKey = getResourceDeploymentKeys();
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const resourceDeploymentKey = getResourceDeploymentKeys(dotMetricsEnabled);
const [selectedEnvironments, setSelectedEnvironments] = useState<string[]>([]);
@@ -66,20 +73,21 @@ function ResourceAttributesFilter({
}, [queries, resourceDeploymentKey]);
useEffect(() => {
getEnvironmentTagKeys().then((tagKeys) => {
getEnvironmentTagKeys(dotMetricsEnabled).then((tagKeys) => {
if (tagKeys && Array.isArray(tagKeys) && tagKeys.length > 0) {
getEnvironmentTagValues().then((tagValues) => {
getEnvironmentTagValues(dotMetricsEnabled).then((tagValues) => {
setEnvironments(tagValues);
});
}
});
}, []);
}, [dotMetricsEnabled]);
return (
<div className="resourceAttributesFilter-container">
<div className="environment-selector">
<Select
getPopupContainer={popupContainer}
key={selectedEnvironments.join('')}
showSearch
mode="multiple"
value={selectedEnvironments}

View File

@@ -1,175 +0,0 @@
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Router } from 'react-router-dom';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ROUTES from 'constants/routes';
import { createMemoryHistory, MemoryHistory } from 'history';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
import { encode } from 'js-base64';
import ResourceAttributesFilter from '../ResourceAttributesFilter';
jest.mock('lib/history', () => ({
__esModule: true,
default: {
push: jest.fn(),
location: { search: '', pathname: '/' },
},
}));
jest.mock('api/metrics/getResourceAttributes', () => ({
getResourceAttributesTagKeys: jest.fn(),
getResourceAttributesTagValues: jest.fn(),
}));
// eslint-disable-next-line import/first, import/order
import {
getResourceAttributesTagKeys,
getResourceAttributesTagValues,
// eslint-disable-next-line import/newline-after-import
} from 'api/metrics/getResourceAttributes';
// eslint-disable-next-line import/first, import/order
import history from 'lib/history';
const mockTagKeys = getResourceAttributesTagKeys as jest.MockedFunction<
typeof getResourceAttributesTagKeys
>;
const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
typeof getResourceAttributesTagValues
>;
function tagKeysPayload(keys: string[]): never {
return {
statusCode: 200,
error: null,
message: 'ok',
payload: {
data: {
attributeKeys: keys.map((key) => ({
key,
dataType: 'string',
type: 'resource',
isColumn: false,
})),
},
},
} as unknown as never;
}
function tagValuesPayload(values: string[]): never {
return {
statusCode: 200,
error: null,
message: 'ok',
payload: { data: { stringAttributeValues: values } },
} as unknown as never;
}
function seedUrl(queries: IResourceAttribute[], pathname: string): void {
const location = history.location as { search: string; pathname: string };
location.search = queries.length
? `?resourceAttribute=${encode(JSON.stringify(queries))}`
: '';
location.pathname = pathname;
}
function renderFilter(pathname: string): MemoryHistory {
const routerHistory = createMemoryHistory({
initialEntries: [`${pathname}${history.location.search}`],
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function Wrapper({ children }: { children: ReactNode }): JSX.Element {
return (
<QueryClientProvider client={queryClient}>
<Router history={routerHistory}>
<ResourceProvider>{children}</ResourceProvider>
</Router>
</QueryClientProvider>
);
}
render(
<Wrapper>
<ResourceAttributesFilter />
</Wrapper>,
);
return routerHistory;
}
describe('ResourceAttributesFilter', () => {
beforeEach(() => {
mockTagKeys.mockReset();
mockTagValues.mockReset();
mockTagKeys.mockResolvedValue(
tagKeysPayload(['resource_deployment.environment']),
);
mockTagValues.mockResolvedValue(tagValuesPayload(['production', 'staging']));
seedUrl([], '/');
});
it('shows every applied filter on the service map, including ones it cannot apply', async () => {
seedUrl(
[
{
id: 'svc',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'env',
tagKey: 'resource_deployment.environment',
operator: 'IN',
tagValue: ['production'],
},
],
ROUTES.SERVICE_MAP,
);
renderFilter(ROUTES.SERVICE_MAP);
await waitFor(() =>
expect(screen.getByText(/service\.name/)).toBeInTheDocument(),
);
await waitFor(() =>
expect(
screen
.getByTestId('resource-environment-filter')
.querySelector('.ant-select-selection-item'),
).toHaveTextContent('production'),
);
});
it('keeps the environment dropdown open so more than one environment can be picked', async () => {
const user = userEvent.setup();
renderFilter('/services');
const environmentFilter = screen.getByTestId('resource-environment-filter');
await user.click(
environmentFilter.querySelector('input') as HTMLInputElement,
);
await user.click(await screen.findByTitle('production'));
await waitFor(() =>
expect(
screen.getByTitle('staging').closest('.ant-select-dropdown'),
).not.toHaveClass('ant-select-dropdown-hidden'),
);
await user.click(screen.getByTitle('staging'));
await waitFor(() => {
const selected = Array.from(
environmentFilter.querySelectorAll('.ant-select-selection-item-content'),
).map((node) => node.textContent);
expect(selected).toStrictEqual(['production', 'staging']);
});
});
});

View File

@@ -3,6 +3,8 @@ import {
getResourceDeploymentKeys,
} from 'hooks/useResourceAttribute/utils';
import { FeatureKeys } from '../../../../constants/features';
import { useAppContext } from '../../../../providers/App/App';
import { QueryChipContainer, QueryChipItem } from '../../styles';
import { IQueryChipProps } from './types';
@@ -11,7 +13,13 @@ function QueryChip({ queryData, onClose }: IQueryChipProps): JSX.Element {
onClose(queryData.id);
};
const isClosable = queryData.tagKey !== getResourceDeploymentKeys();
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const isClosable =
queryData.tagKey !== getResourceDeploymentKeys(dotMetricsEnabled);
return (
<QueryChipContainer>

View File

@@ -4,6 +4,8 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { ServiceMetricsProps } from '../types';
import { getQueryRangeRequestData } from '../utils';
import ServiceMetricTable from './ServiceMetricTable';
@@ -16,13 +18,19 @@ function ServiceMetricsApplication({
GlobalReducer
>((state) => state.globalTime);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryRangeRequestData = useMemo(
() =>
getQueryRangeRequestData({
topLevelOperations,
globalSelectedInterval,
dotMetricsEnabled,
}),
[globalSelectedInterval, topLevelOperations],
[globalSelectedInterval, topLevelOperations, dotMetricsEnabled],
);
return (
<ServiceMetricTable

View File

@@ -19,10 +19,13 @@ import {
export const serviceMetricsQuery = (
topLevelOperation: [keyof ServiceDataProps, string[]],
dotMetricsEnabled: boolean,
): QueryBuilderData => {
const p99AutoCompleteData: BaseAutocompleteData = {
dataType: DataTypes.Float64,
key: WidgetKeys.SignozLatencyBucket,
key: dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm,
type: '',
};
@@ -50,7 +53,9 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -73,7 +78,9 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -83,7 +90,7 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.Int64,
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
@@ -106,7 +113,9 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -129,7 +138,9 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -182,7 +193,9 @@ export const serviceMetricsQuery = (
const groupBy: BaseAutocompleteData[] = [
{
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Tag,
},
];

View File

@@ -17,6 +17,8 @@ import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import ServiceTraceTable from './ServiceTracesTable';
@@ -38,6 +40,11 @@ function ServiceTraces(): JSX.Element {
selectedTags,
});
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
useErrorNotification(error);
const services = data || [];
@@ -55,7 +62,7 @@ function ServiceTraces(): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
const rps = data.reduce((total, service) => total + service.callRate, 0);

View File

@@ -26,6 +26,7 @@ export interface ServiceMetricsTableProps {
export interface GetQueryRangeRequestDataProps {
topLevelOperations: [keyof ServiceDataProps, string[]][];
globalSelectedInterval: Time | CustomTimeType;
dotMetricsEnabled: boolean;
}
export interface GetServiceListFromQueryProps {

View File

@@ -26,6 +26,7 @@ export function getSeriesValue(
export const getQueryRangeRequestData = ({
topLevelOperations,
globalSelectedInterval,
dotMetricsEnabled,
}: GetQueryRangeRequestDataProps): GetQueryResultsProps[] => {
const requestData: GetQueryResultsProps[] = [];
topLevelOperations.forEach((operation) => {
@@ -33,7 +34,7 @@ export const getQueryRangeRequestData = ({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: serviceMetricsQuery(operation),
builder: serviceMetricsQuery(operation, dotMetricsEnabled),
clickhouse_sql: [],
id: uuid(),
},

View File

@@ -27,6 +27,7 @@ export type WhereClauseConfig = {
export const useAutoComplete = (
query: IBuilderQuery,
dotMetricsEnabled: boolean,
whereClauseConfig?: WhereClauseConfig,
shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,
@@ -39,6 +40,7 @@ export const useAutoComplete = (
const { keys, results, isFetching, exampleQueries } = useFetchKeysAndValues(
searchValue,
query,
dotMetricsEnabled,
searchKey,
shouldUseSuggestions,
isInfraMonitoring,

View File

@@ -48,6 +48,7 @@ type IuseFetchKeysAndValues = {
export const useFetchKeysAndValues = (
searchValue: string,
query: IBuilderQuery,
dotMetricsEnabled: boolean,
searchKey: string,
shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,

View File

@@ -1,10 +1,14 @@
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { encode } from 'js-base64';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
import { whilelistedKeys } from './config';
import { ResourceContext } from './context';
import {
IResourceAttribute,
@@ -54,6 +58,11 @@ function ResourceProvider({ children }: Props): JSX.Element {
}
};
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const dispatchQueries = useCallback(
(queries: IResourceAttribute[]): void => {
urlQuery.set(
@@ -69,7 +78,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
const loadTagKeys = (): void => {
handleLoading(true);
GetTagKeys()
GetTagKeys(dotMetricsEnabled)
.then((tagKeys) => {
const options = mappingWithRoutesAndKeys(pathname, tagKeys);
setOptionsData({ options, mode: undefined });
@@ -152,15 +161,15 @@ function ResourceProvider({ children }: Props): JSX.Element {
setSelectedQueries([...value]);
},
[optionsData.mode, step, staging, pathname],
[optionsData.mode, step, staging, dotMetricsEnabled, pathname],
);
const handleEnvironmentChange = useCallback(
(environments: string[]): void => {
const staging = [getResourceDeploymentKeys(), 'IN'];
const staging = [getResourceDeploymentKeys(dotMetricsEnabled), 'IN'];
const queriesCopy = queries.filter(
(query) => query.tagKey !== getResourceDeploymentKeys(),
(query) => query.tagKey !== getResourceDeploymentKeys(dotMetricsEnabled),
);
if (environments && Array.isArray(environments) && environments.length > 0) {
@@ -175,7 +184,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
setStep('Idle');
},
[dispatchQueries, queries],
[dispatchQueries, dotMetricsEnabled, queries],
);
const handleClose = useCallback(
@@ -193,9 +202,16 @@ function ResourceProvider({ children }: Props): JSX.Element {
setOptionsData({ mode: undefined, options: [] });
}, [dispatchQueries]);
const getVisibleQueries = useMemo(() => {
if (pathname === ROUTES.SERVICE_MAP) {
return queries.filter((query) => whilelistedKeys.includes(query.tagKey));
}
return queries;
}, [queries, pathname]);
const value: IResourceAttributeProps = useMemo(
() => ({
queries,
queries: getVisibleQueries,
staging,
handleClearAll,
handleClose,
@@ -218,7 +234,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
staging,
selectedQuery,
optionsData,
queries,
getVisibleQueries,
],
);

View File

@@ -2,9 +2,13 @@ import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Router } from 'react-router-dom';
import { act, renderHook, waitFor } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import { createMemoryHistory, MemoryHistory } from 'history';
import { encode } from 'js-base64';
import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types';
import { getAppContextMock } from 'tests/test-utils';
import ResourceProvider from '../ResourceProvider';
import useResourceAttribute from '../useResourceAttribute';
@@ -51,8 +55,10 @@ const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
function createWrapper({
routerHistory,
appContextOverrides,
}: {
routerHistory: MemoryHistory;
appContextOverrides?: Partial<IAppContext>;
}): ({ children }: { children: ReactNode }) => JSX.Element {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
@@ -60,9 +66,13 @@ function createWrapper({
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
return (
<QueryClientProvider client={queryClient}>
<Router history={routerHistory}>
<ResourceProvider>{children}</ResourceProvider>
</Router>
<AppContext.Provider
value={getAppContextMock('ADMIN', appContextOverrides)}
>
<Router history={routerHistory}>
<ResourceProvider>{children}</ResourceProvider>
</Router>
</AppContext.Provider>
</QueryClientProvider>
);
};
@@ -401,7 +411,7 @@ describe('ResourceProvider', () => {
});
describe('handleEnvironmentChange', () => {
it('adds a dotted environment query when envs are provided', async () => {
it('adds an environment query when envs are provided', async () => {
const routerHistory = createMemoryHistory({ initialEntries: ['/'] });
const { result } = renderHook(() => useResourceAttribute(), {
wrapper: createWrapper({ routerHistory }),
@@ -414,7 +424,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
expect(result.current.queries).toHaveLength(1);
expect(result.current.queries[0]).toMatchObject({
tagKey: 'resource_deployment.environment',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
});
@@ -425,7 +435,7 @@ describe('ResourceProvider', () => {
const seeded = [
{
id: 'env',
tagKey: 'resource_deployment.environment',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
},
@@ -449,7 +459,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
const tagKeys = result.current.queries.map((q) => q.tagKey);
expect(tagKeys).not.toContain('resource_deployment.environment');
expect(tagKeys).not.toContain('resource_deployment_environment');
expect(tagKeys).toContain('resource_service_name');
});
});
@@ -458,7 +468,7 @@ describe('ResourceProvider', () => {
const seeded = [
{
id: 'env',
tagKey: 'resource_deployment.environment',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
},
@@ -476,13 +486,43 @@ describe('ResourceProvider', () => {
await waitFor(() => {
const envQueries = result.current.queries.filter(
(q) => q.tagKey === 'resource_deployment.environment',
(q) => q.tagKey === 'resource_deployment_environment',
);
expect(envQueries).toHaveLength(1);
expect(envQueries[0].tagValue).toStrictEqual(['staging']);
});
});
it('uses the dotted deployment env key when DOT_METRICS_ENABLED is active', async () => {
const routerHistory = createMemoryHistory({ initialEntries: ['/'] });
const { result } = renderHook(() => useResourceAttribute(), {
wrapper: createWrapper({
routerHistory,
appContextOverrides: {
featureFlags: [
{
name: FeatureKeys.DOT_METRICS_ENABLED,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
},
}),
});
act(() => {
result.current.handleEnvironmentChange(['production']);
});
await waitFor(() => {
expect(result.current.queries[0].tagKey).toBe(
'resource_deployment.environment',
);
});
});
it('preserves unrelated query params when dispatching', async () => {
const routerHistory = createMemoryHistory({
initialEntries: ['/?tab=overview'],
@@ -504,23 +544,22 @@ describe('ResourceProvider', () => {
});
});
describe('SERVICE_MAP', () => {
const seeded = [
{
id: 'a',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'b',
tagKey: 'resource_k8s_cluster_name',
operator: 'IN',
tagValue: ['prod'],
},
];
it('exposes every query from the URL, including ones the map cannot apply', () => {
describe('getVisibleQueries (SERVICE_MAP filtering)', () => {
it('filters queries down to whitelisted keys on SERVICE_MAP', () => {
const seeded = [
{
id: 'a',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'b',
tagKey: 'resource_k8s_cluster_name',
operator: 'IN',
tagValue: ['prod'],
},
];
mockLibHistory(
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
ROUTES.SERVICE_MAP,
@@ -533,10 +572,24 @@ describe('ResourceProvider', () => {
wrapper: createWrapper({ routerHistory }),
});
expect(result.current.queries).toStrictEqual(seeded);
expect(result.current.queries).toStrictEqual([seeded[1]]);
});
it('returns all queries on non-SERVICE_MAP routes', () => {
const seeded = [
{
id: 'a',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'b',
tagKey: 'resource_k8s_cluster_name',
operator: 'IN',
tagValue: ['prod'],
},
];
mockLibHistory(
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
'/services',

View File

@@ -1,20 +1,17 @@
import ROUTES from 'constants/routes';
import { whilelistedKeys } from '../config';
import {
filterServiceMapSupportedQueries,
mappingWithRoutesAndKeys,
} from '../utils';
import { mappingWithRoutesAndKeys } from '../utils';
describe('useResourceAttribute config', () => {
describe('whilelistedKeys', () => {
it('should include underscore-notation keys', () => {
it('should include underscore-notation keys (DOT_METRICS_ENABLED=false)', () => {
expect(whilelistedKeys).toContain('resource_deployment_environment');
expect(whilelistedKeys).toContain('resource_k8s_cluster_name');
expect(whilelistedKeys).toContain('resource_k8s_cluster_namespace');
});
it('should include dot-notation keys', () => {
it('should include dot-notation keys (DOT_METRICS_ENABLED=true)', () => {
expect(whilelistedKeys).toContain('resource_deployment.environment');
expect(whilelistedKeys).toContain('resource_k8s.cluster.name');
expect(whilelistedKeys).toContain('resource_k8s.cluster.namespace');
@@ -77,29 +74,4 @@ describe('useResourceAttribute config', () => {
expect(result).toStrictEqual(allFilters);
});
});
describe('filterServiceMapSupportedQueries', () => {
const environmentQuery = {
id: 'env',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
};
const serviceQuery = {
id: 'svc',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
};
it('should keep only the queries the service map can filter on', () => {
expect(
filterServiceMapSupportedQueries([environmentQuery, serviceQuery]),
).toStrictEqual([environmentQuery]);
});
it('should return an empty list when no query is supported', () => {
expect(filterServiceMapSupportedQueries([serviceQuery])).toStrictEqual([]);
});
});
});

View File

@@ -144,11 +144,19 @@ export const OperatorSchema: IOption[] = OperatorConversions.map(
}),
);
export const getResourceDeploymentKeys = (): string =>
'resource_deployment.environment';
export const getResourceDeploymentKeys = (
dotMetricsEnabled: boolean,
): string => {
if (dotMetricsEnabled) {
return 'resource_deployment.environment';
}
return 'resource_deployment_environment';
};
export const GetTagKeys = async (): Promise<IOption[]> => {
const resourceDeploymentKey = getResourceDeploymentKeys();
export const GetTagKeys = async (
dotMetricsEnabled: boolean,
): Promise<IOption[]> => {
const resourceDeploymentKey = getResourceDeploymentKeys(dotMetricsEnabled);
const { payload } = await getResourceAttributesTagKeys({
metricName: 'signoz_calls_total',
match: 'resource_',
@@ -168,10 +176,12 @@ export const GetTagKeys = async (): Promise<IOption[]> => {
}));
};
export const getEnvironmentTagKeys = async (): Promise<IOption[]> => {
export const getEnvironmentTagKeys = async (
dotMetricsEnabled: boolean,
): Promise<IOption[]> => {
const { payload } = await getResourceAttributesTagKeys({
metricName: 'signoz_calls_total',
match: getResourceDeploymentKeys(),
match: getResourceDeploymentKeys(dotMetricsEnabled),
});
if (!payload || !payload?.data) {
return [];
@@ -184,9 +194,11 @@ export const getEnvironmentTagKeys = async (): Promise<IOption[]> => {
}));
};
export const getEnvironmentTagValues = async (): Promise<IOption[]> => {
export const getEnvironmentTagValues = async (
dotMetricsEnabled: boolean,
): Promise<IOption[]> => {
const { payload } = await getResourceAttributesTagValues({
tagKey: getResourceDeploymentKeys(),
tagKey: getResourceDeploymentKeys(dotMetricsEnabled),
metricName: 'signoz_calls_total',
});
@@ -281,8 +293,3 @@ export const mappingWithRoutesAndKeys = (
}
return filters;
};
export const filterServiceMapSupportedQueries = (
queries: IResourceAttribute[],
): IResourceAttribute[] =>
queries.filter((query) => whilelistedKeys.includes(query.tagKey));

View File

@@ -13,13 +13,11 @@ import { AppProvider } from 'providers/App/App';
import TimezoneProvider from 'providers/Timezone';
import store from 'store';
import APIError from 'types/api/error';
import { installTranslationResilience } from 'translation-resilience';
import './ReactI18';
import 'styles.scss';
installTranslationResilience();
configureOverlayScrollbars();
const queryClient = new QueryClient({

View File

@@ -1,55 +0,0 @@
import { ILog } from 'types/api/logs/log';
import { getLogFieldValue } from './flatLogData';
const asLog = (partial: Partial<ILog>): ILog => partial as unknown as ILog;
describe('getLogFieldValue', () => {
it('resolves a nested body field by dotted key when use_json_body is on', () => {
const log = asLog({ body: { a: { b: { c: 'deep' } } } });
expect(getLogFieldValue(log, 'a.b.c', true)).toBe('deep');
});
it('ignores body when use_json_body is off', () => {
const log = asLog({ body: { a: { b: { c: 'deep' } } } });
expect(getLogFieldValue(log, 'a.b.c', false)).toBeUndefined();
});
it('ignores a stringified body even when use_json_body is on', () => {
const log = asLog({ body: '{"a":{"b":1}}' });
expect(getLogFieldValue(log, 'a.b', true)).toBeUndefined();
});
it('prefers the body value over attributes when the key exists in both (body first)', () => {
const log = asLog({
attributes_string: { 'a.b': 'attr' } as never,
body: { a: { b: 'bodyval' } },
});
expect(getLogFieldValue(log, 'a.b', true)).toBe('bodyval');
});
it('falls back to attributes when the key is not in the body', () => {
const log = asLog({
attributes_string: { 'x.y': 'attr' } as never,
body: { other: 1 },
});
expect(getLogFieldValue(log, 'x.y', true)).toBe('attr');
});
it('preserves falsy body values (0, false, empty string)', () => {
const log = asLog({ body: { n: 0, flag: false, s: '' } });
expect(getLogFieldValue(log, 'n', true)).toBe(0);
expect(getLogFieldValue(log, 'flag', true)).toBe(false);
expect(getLogFieldValue(log, 's', true)).toBe('');
});
it('returns undefined when the body path is missing', () => {
const log = asLog({ body: { x: 1 } });
expect(getLogFieldValue(log, 'nope', true)).toBeUndefined();
});
it('returns undefined when a mid path segment is not an object', () => {
const log = asLog({ body: { a: { b: 'leaf' } } });
expect(getLogFieldValue(log, 'a.b.c', true)).toBeUndefined();
});
});

View File

@@ -1,5 +1,5 @@
import { defaultTo } from 'lodash-es';
import { ILog, ILogBody } from 'types/api/logs/log';
import { ILog } from 'types/api/logs/log';
export function FlatLogData(log: ILog): Record<string, string> {
const flattenLogObject: Record<string, string> = {};
@@ -15,29 +15,3 @@ export function FlatLogData(log: ILog): Record<string, string> {
});
return flattenLogObject;
}
function getBodyFieldValue(body: ILogBody, key: string): unknown {
return key.split('.').reduce<unknown>((acc, segment) => {
if (acc && typeof acc === 'object' && !Array.isArray(acc)) {
return (acc as Record<string, unknown>)[segment];
}
return undefined;
}, body);
}
// Resolve one field for the logs table. A JSON body is checked first (use_json_body
// only), splitting the key on `.`; otherwise fall back to FlatLogData
// (attributes/resources/scope/top-level).
export function getLogFieldValue(
log: ILog,
fieldName: string,
isBodyJsonEnabled: boolean,
): unknown {
if (isBodyJsonEnabled && log.body && typeof log.body === 'object') {
const bodyValue = getBodyFieldValue(log.body, fieldName);
if (bodyValue !== undefined) {
return bodyValue;
}
}
return FlatLogData(log)[fieldName];
}

View File

@@ -34,7 +34,7 @@ describe('normalizeFilterExpression', () => {
);
});
it('lowercases HAS / HASANY / HASALL / HASTOKEN / SEARCH function names', () => {
it('lowercases HAS / HASANY / HASALL / HASTOKEN function names', () => {
expect(normalizeFilterExpression('HAS(tags, "x")')).toBe(
normalizeFilterExpression('has(tags, "x")'),
);
@@ -47,9 +47,6 @@ describe('normalizeFilterExpression', () => {
expect(normalizeFilterExpression('HASTOKEN(msg, "err")')).toBe(
normalizeFilterExpression('hasToken(msg, "err")'),
);
expect(normalizeFilterExpression('SEARCH("err")')).toBe(
normalizeFilterExpression('search("err")'),
);
});
it('lowercases TRUE / FALSE boolean literals', () => {

View File

@@ -1,4 +1,5 @@
import { getToolTipValue } from 'components/Graph/yAxisConfig';
import { PANEL_TYPES } from 'constants/queryBuilder';
import uPlot, { Axis } from 'uplot';
import { uPlotXAxisValuesFormat } from '../../uPlotLib/utils/constants';
@@ -6,6 +7,11 @@ import getGridColor from '../../uPlotLib/utils/getGridColor';
import { buildYAxisSizeCalculator } from '../utils/axis';
import { AxisProps, ConfigBuilder } from './types';
const PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT = [
PANEL_TYPES.TIME_SERIES,
PANEL_TYPES.BAR,
];
/**
* Builder for uPlot axis configuration
* Handles creation and merging of axis settings
@@ -61,9 +67,12 @@ export class UPlotAxisBuilder extends ConfigBuilder<AxisProps, Axis> {
* Build values formatter for X-axis (time)
*/
private buildXAxisValuesFormatter(): uPlot.Axis.Values | undefined {
const { isTimeAxis } = this.props;
const { panelType } = this.props;
if (isTimeAxis) {
if (
panelType &&
PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT.includes(panelType)
) {
return uPlotXAxisValuesFormat as uPlot.Axis.Values;
}

View File

@@ -1,4 +1,5 @@
import { getToolTipValue } from 'components/Graph/yAxisConfig';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { uPlotXAxisValuesFormat } from 'lib/uPlotLib/utils/constants';
import type uPlot from 'uplot';
@@ -136,11 +137,11 @@ describe('UPlotAxisBuilder', () => {
});
});
it('uses time-based X-axis values formatter when the caller declares a time axis', () => {
it('uses time-based X-axis values formatter for time-series like panels', () => {
const builder = new UPlotAxisBuilder(
createAxisProps({
scaleKey: 'x',
isTimeAxis: true,
panelType: PANEL_TYPES.TIME_SERIES,
}),
);
@@ -149,11 +150,11 @@ describe('UPlotAxisBuilder', () => {
expect(config.values).toBe(uPlotXAxisValuesFormat);
});
it('does not attach X-axis datetime formatter for a non-time axis', () => {
it('does not attach X-axis datetime formatter when panel type is not supported', () => {
const builder = new UPlotAxisBuilder(
createAxisProps({
scaleKey: 'x',
isTimeAxis: false,
panelType: PANEL_TYPES.LIST, // not in PANEL_TYPES_WITH_X_AXIS_DATETIME_FORMAT
}),
);
@@ -289,9 +290,22 @@ describe('UPlotAxisBuilder', () => {
expect(config.space).toBe(50);
});
it('omits the X-axis datetime formatter when no time axis is declared', () => {
const builder = new UPlotAxisBuilder(createAxisProps({ scaleKey: 'x' }));
expect(builder.getConfig().values).toBeUndefined();
it('includes PANEL_TYPES.BAR and PANEL_TYPES.TIME_SERIES in X-axis datetime formatter', () => {
const barBuilder = new UPlotAxisBuilder(
createAxisProps({
scaleKey: 'x',
panelType: PANEL_TYPES.BAR,
}),
);
expect(barBuilder.getConfig().values).toBe(uPlotXAxisValuesFormat);
const timeSeriesBuilder = new UPlotAxisBuilder(
createAxisProps({
scaleKey: 'x',
panelType: PANEL_TYPES.TIME_SERIES,
}),
);
expect(timeSeriesBuilder.getConfig().values).toBe(uPlotXAxisValuesFormat);
});
it('should return the existing size when cycleNum > 1', () => {

View File

@@ -1,4 +1,5 @@
import { PrecisionOption } from 'components/Graph/types';
import { PANEL_TYPES } from 'constants/queryBuilder';
import uPlot, { Series } from 'uplot';
import { ThresholdsDrawHookOptions } from '../hooks/types';
@@ -69,12 +70,7 @@ export interface AxisProps {
isDarkMode?: boolean;
isLogScale?: boolean;
yAxisUnit?: string;
/**
* X axis carries timestamps, so its ticks format as dates/times. Declared by the caller
* rather than inferred from a panel type — a chart whose x axis is buckets or categories
* (histogram) leaves it off.
*/
isTimeAxis?: boolean;
panelType?: PANEL_TYPES;
decimalPrecision?: PrecisionOption;
}

View File

@@ -0,0 +1,218 @@
export const membersResponse = [
{
id: '3223a874-5678458745786',
name: 'John Doe',
email: 'firstUser@test.io',
createdAt: 1666357530,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '5e9681b1-5678458745786',
name: 'Jane Doe',
email: 'johndoe2@test.io',
createdAt: 1666365394,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '11e8c55d-5678458745786',
name: 'Alex',
email: 'blah@test.io',
createdAt: 1666366317,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: 'd878012367813286731aab62',
role: 'VIEWER',
organization: 'Test Inc',
flags: null,
},
{
id: '2ad2e404-5678458745786',
name: 'Tom',
email: 'johndoe4@test.io',
createdAt: 1673441483,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '6f532456-5678458745786',
name: 'Harry',
email: 'harry@test.io',
createdAt: 1691551672,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: 'ae22fa73-5678458745786',
name: 'Ron',
email: 'ron@test.io',
createdAt: 1691668239,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '3223a874-5678458745786',
name: 'John Doe',
email: 'johndoe@test.io',
createdAt: 1666357530,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '5e9681b1-5678458745786',
name: 'Jane Doe',
email: 'johndoe2@test.io',
createdAt: 1666365394,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '11e8c55d-5678458745786',
name: 'Alex',
email: 'blah@test.io',
createdAt: 1666366317,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: 'd878012367813286731aab62',
role: 'VIEWER',
organization: 'Test Inc',
flags: null,
},
{
id: '2ad2e404-5678458745786',
name: 'Tom',
email: 'johndoe4@test.io',
createdAt: 1673441483,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '6f532456-5678458745786',
name: 'Harry',
email: 'harry@test.io',
createdAt: 1691551672,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: 'ae22fa73-5678458745786',
name: 'Ron',
email: 'ron@test.io',
createdAt: 1691668239,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '3223a874-5678458745786',
name: 'John Doe',
email: 'johndoe@test.io',
createdAt: 1666357530,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '5e9681b1-5678458745786',
name: 'Jane Doe',
email: 'johndoe2@test.io',
createdAt: 1666365394,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '11e8c55d-5678458745786',
name: 'Alex',
email: 'blah@test.io',
createdAt: 1666366317,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: 'd878012367813286731aab62',
role: 'VIEWER',
organization: 'Test Inc',
flags: null,
},
{
id: '2ad2e404-5678458745786',
name: 'Tom',
email: 'johndoe4@test.io',
createdAt: 1673441483,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '6f532456-5678458745786',
name: 'Harry',
email: 'harry@test.io',
createdAt: 1691551672,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: 'ae22fa73-5678458745786',
name: 'Ron',
email: 'lastUser@test.io',
createdAt: 1691668239,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
];

View File

@@ -12,6 +12,7 @@ import {
} from './__mockdata__/dashboards';
import { explorerView } from './__mockdata__/explorer_views';
import { licensesSuccessResponse } from './__mockdata__/licenses';
import { membersResponse } from './__mockdata__/members';
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
import { serviceSuccessResponse } from './__mockdata__/services';
import { topLevelOperationSuccessResponse } from './__mockdata__/top_level_operations';
@@ -39,6 +40,9 @@ export const handlers = [
res(ctx.status(200), ctx.json(topLevelOperationSuccessResponse)),
),
rest.get('http://localhost/api/v1/user', (req, res, ctx) =>
res(ctx.status(200), ctx.json({ status: '200', data: membersResponse })),
),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_keys',
(req, res, ctx) => {
@@ -160,6 +164,15 @@ export const handlers = [
),
),
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: 'invite sent successfully',
}),
),
),
rest.get(
'http://localhost/api/v3/autocomplete/aggregate_attributes',
(req, res, ctx) =>

View File

@@ -1,6 +1,6 @@
//@ts-nocheck
import { useEffect, useMemo, useRef } from 'react';
import { useEffect, useRef } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
@@ -11,7 +11,6 @@ import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { whilelistedKeys } from 'hooks/useResourceAttribute/config';
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
import { filterServiceMapSupportedQueries } from 'hooks/useResourceAttribute/utils';
import { getDetailedServiceMapItems, ServiceMapStore } from 'store/actions';
import { AppState } from 'store/reducers';
import styled from 'styled-components';
@@ -71,37 +70,32 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
const { queries } = useResourceAttribute();
const supportedQueries = useMemo(
() => filterServiceMapSupportedQueries(queries),
[queries],
);
useEffect(() => {
/*
Call the apis only when the route is loaded.
Check this issue: https://github.com/SigNoz/signoz/issues/110
*/
getDetailedServiceMapItems(globalTime, supportedQueries);
}, [globalTime, getDetailedServiceMapItems, supportedQueries]);
getDetailedServiceMapItems(globalTime, queries);
}, [globalTime, getDetailedServiceMapItems, queries]);
useEffect(() => {
fgRef.current && fgRef.current.d3Force('charge').strength(-400);
});
const renderBody = (): JSX.Element => {
if (serviceMap.loading) {
return <Spinner size="large" tip="Loading..." />;
}
if (serviceMap.items.length === 0) {
return <Card>No Service Found</Card>;
}
return <Map fgRef={fgRef} serviceMap={serviceMap} />;
};
if (serviceMap.loading) {
return <Spinner size="large" tip="Loading..." />;
}
if (!serviceMap.loading && serviceMap.items.length === 0) {
return (
<Container>
<ResourceAttributesFilter />
<Card>No Service Found</Card>
</Container>
);
}
return (
<Container className="service-map-container">
<div className="service-map-container">
<ResourceAttributesFilter
suffixIcon={
<TextToolTip
@@ -114,8 +108,8 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
}
/>
{renderBody()}
</Container>
<Map fgRef={fgRef} serviceMap={serviceMap} />
</div>
);
}

View File

@@ -13,6 +13,7 @@ import type { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.sche
import PromQLIcon from 'assets/Dashboard/PromQl';
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ClickHouseQueryContainer from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse';
import PromQLQueryContainer from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
@@ -25,7 +26,6 @@ import {
getHiddenQueryBuilderFields,
getSupportedQueryTypes,
} from '../../Panels/capabilities';
import { getPanelDefinition } from '../../Panels/registry';
import {
PANEL_KIND_TO_PANEL_TYPE,
type PanelKind,
@@ -64,10 +64,8 @@ function PanelEditorQueryBuilder({
footer,
stickyHeader = true,
}: PanelEditorQueryBuilderProps): JSX.Element {
// The shared QueryBuilderV2 provider still speaks the legacy PANEL_TYPES; what the
// builder offers for this kind comes from the kind's own declaration.
// The shared QueryBuilderV2 / list-view checks still speak the legacy PANEL_TYPES.
const panelType = PANEL_KIND_TO_PANEL_TYPE[panelKind];
const { listView, traceOperator } = getPanelDefinition(panelKind).query;
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const isDarkMode = useIsDarkMode();
@@ -114,9 +112,9 @@ function PanelEditorQueryBuilder({
<QueryBuilderV2
panelType={panelType}
filterConfigs={filterConfigs}
showTraceOperator={traceOperator}
showTraceOperator={panelType !== PANEL_TYPES.LIST}
version="v3"
isListViewPanel={listView}
isListViewPanel={panelType === PANEL_TYPES.LIST}
queryComponents={{}}
signalSourceChangeEnabled
savePreviousQuery

View File

@@ -1,26 +1,26 @@
import { Spline } from '@signozhq/icons';
import { PANEL_TYPES } from 'constants/queryBuilder';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import { EQueryType } from 'types/common/dashboard';
interface PlotTagProps {
/** Authoring mode of the panel's query; undefined when no query exists yet. */
queryType: EQueryType | undefined;
/** Kind is authored as a list view — nothing is plotted, so the chip has nothing to say. */
isListView: boolean;
panelType: PANEL_TYPES;
className?: string;
}
/**
* "Plotted with <query mode>" chip for the editor preview; V2 counterpart of V1's
* PlotTag (duplicated per the split policy). Hidden for list views and before a
* PlotTag (duplicated per the split policy). Hidden for list panels and before a
* query exists, where the mode is irrelevant.
*/
function PlotTag({
queryType,
isListView,
panelType,
className,
}: PlotTagProps): JSX.Element | null {
if (queryType === undefined || isListView) {
if (queryType === undefined || panelType === PANEL_TYPES.LIST) {
return null;
}

View File

@@ -7,6 +7,7 @@ import PanelBody from 'pages/DashboardPageV2/DashboardContainer/PanelsAndSection
import PanelHeader from 'pages/DashboardPageV2/DashboardContainer/PanelsAndSectionsLayout/Panel/PanelHeader/PanelHeader';
import type { AnyPanelInteractionProps } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/interactions';
import type { RenderablePanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/panelDefinition';
import { PANEL_KIND_TO_PANEL_TYPE } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/panelKind';
import type { DashboardPreference } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/rendererProps';
import { getPanelQueryType } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/getPanelQueryType';
import type {
@@ -71,6 +72,7 @@ function PreviewPane({
onClick,
enableDrillDown,
}: PreviewPaneProps): JSX.Element {
const panelType = PANEL_KIND_TO_PANEL_TYPE[panel.spec.plugin.kind];
const queryType = getPanelQueryType(panel);
// Search term is ephemeral preview state, threaded to header + renderer but
@@ -84,7 +86,7 @@ function PreviewPane({
<div className={styles.header}>
<PlotTag
queryType={queryType}
isListView={panelDefinition.query.listView}
panelType={panelType}
className={styles.queryType}
/>
<div className={styles.dateTimeSelector}>

View File

@@ -1,22 +1,30 @@
import { render, screen } from '@testing-library/react';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { EQueryType } from 'types/common/dashboard';
import PlotTag from '../PlotTag';
describe('PlotTag', () => {
it('renders the resolved query mode', () => {
render(<PlotTag queryType={EQueryType.PROM} isListView={false} />);
render(
<PlotTag queryType={EQueryType.PROM} panelType={PANEL_TYPES.TIME_SERIES} />,
);
expect(screen.getByTestId('panel-editor-plot-tag')).toBeInTheDocument();
expect(screen.getByText('PromQL')).toBeInTheDocument();
});
it('renders nothing when there is no query yet', () => {
render(<PlotTag queryType={undefined} isListView={false} />);
render(<PlotTag queryType={undefined} panelType={PANEL_TYPES.TIME_SERIES} />);
expect(screen.queryByTestId('panel-editor-plot-tag')).not.toBeInTheDocument();
});
it('renders nothing for a list view (query mode is irrelevant)', () => {
render(<PlotTag queryType={EQueryType.QUERY_BUILDER} isListView />);
it('renders nothing for list panels (query mode is irrelevant)', () => {
render(
<PlotTag
queryType={EQueryType.QUERY_BUILDER}
panelType={PANEL_TYPES.LIST}
/>,
);
expect(screen.queryByTestId('panel-editor-plot-tag')).not.toBeInTheDocument();
});
});

View File

@@ -4,10 +4,7 @@ import type {
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { PANEL_TYPES } from 'constants/queryBuilder';
import {
getPanelDefinition,
isPanelKindSupported,
} from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
import { getPanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/registry';
import type { RenderablePanelDefinition } from 'pages/DashboardPageV2/DashboardContainer/Panels/types/panelDefinition';
import {
PANEL_KIND_TO_PANEL_TYPE,
@@ -94,9 +91,8 @@ export function usePanelEditSession({
const query = usePanelQuery({
panel: draft,
panelId,
queryCapabilities: panelDefinition.query,
time,
enabled: isPanelKindSupported(panelKind),
enabled: !!panelDefinition,
});
const { runQuery, isQueryDirty, buildSaveSpec } = usePanelEditorQuerySync({

View File

@@ -6,7 +6,7 @@ import type {
DashboardtypesQueryDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { PANEL_TYPES } from 'constants/queryBuilder';
import { PANEL_TYPES } from 'constants/queryBuilder';
import {
handleQueryChange,
type PartialPanelTypes,
@@ -19,7 +19,6 @@ import type {
} from 'types/api/queryBuilder/queryBuilderData';
import { resolveQueryType } from '../../Panels/capabilities';
import { getPanelDefinition } from '../../Panels/registry';
import {
PANEL_KIND_TO_PANEL_TYPE,
type PanelKind,
@@ -145,10 +144,11 @@ export function usePanelTypeSwitch({
{ ...query, queryType },
panelTypeRef.current,
);
// Match a fresh list view's default order so the builder's Order By isn't empty.
const nextQuery = getPanelDefinition(newKind).query.listView
? withDefaultListOrder(transformed)
: transformed;
// Match a fresh list panel's default order so the builder's Order By isn't empty.
const nextQuery =
newPanelType === PANEL_TYPES.LIST
? withDefaultListOrder(transformed)
: transformed;
const signal = getBuilderQueries(currentSpec.queries)[0]
?.signal as TelemetrytypesSignalDTO;

View File

@@ -1,14 +1,7 @@
import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { OPERATORS } from 'constants/queryBuilder';
import { EQueryType } from 'types/common/dashboard';
import { UNSUPPORTED_PANEL } from '../kinds/UnsupportedPanel/definition';
import { getPanelDefinition, isPanelKindSupported } from '../registry';
import type { PanelQueryCapabilities } from '../types/panelCapabilities';
import { NO_PANEL_ACTIONS } from '../types/panelDefinition';
import {
getHiddenQueryBuilderFields,
getSupportedQueryTypes,
@@ -22,7 +15,6 @@ import type { PanelKind } from '../types/panelKind';
const { QUERY_BUILDER, CLICKHOUSE, PROM } = EQueryType;
const { logs, traces, metrics } = TelemetrytypesSignalDTO;
const { time_series, scalar, raw } = Querybuildertypesv5RequestTypeDTO;
const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
'signoz/TimeSeriesPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
@@ -45,131 +37,9 @@ const EXPECTED_SIGNALS: Record<PanelKind, TelemetrytypesSignalDTO[]> = {
'signoz/ListPanel': [logs, traces],
};
// Exhaustive over PanelKind, so a new kind can't ship without stating how its request is
// shaped — the check that used to be implicit in a legacy PANEL_TYPES switch.
const EXPECTED_QUERY_CAPABILITIES: Record<PanelKind, PanelQueryCapabilities> = {
'signoz/TimeSeriesPanel': {
requestType: time_series,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
// Bar bins client-side, so it asks for a widened step interval over a raw series.
'signoz/BarChartPanel': {
requestType: time_series,
formatTableResultForUI: false,
bucketedStepInterval: true,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
'signoz/HistogramPanel': {
requestType: time_series,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
'signoz/NumberPanel': {
requestType: scalar,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
'signoz/PieChartPanel': {
requestType: scalar,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
// Only Table asks the server to transpose its scalar result into UI rows.
'signoz/TablePanel': {
requestType: scalar,
formatTableResultForUI: true,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
// Only List reads raw rows, pages them server-side, and needs an order tiebreaker.
'signoz/ListPanel': {
requestType: raw,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: true,
serverPaginated: true,
listView: true,
traceOperator: false,
},
};
const ALL_KINDS = Object.keys(EXPECTED_QUERY_TYPES) as PanelKind[];
describe('panel capabilities guard', () => {
describe('query capabilities', () => {
it.each(ALL_KINDS)('declares how %s shapes its request', (kind) => {
expect(getPanelDefinition(kind).query).toStrictEqual(
EXPECTED_QUERY_CAPABILITIES[kind],
);
});
});
// A dashboard spec written by a newer SigNoz can name a kind this build has no
// definition for. The registry answers with UNSUPPORTED_PANEL rather than nothing, so
// every guard below reads it without first proving a definition exists.
describe('a kind this build cannot render', () => {
const unknownKind = 'signoz/SomeFutureKindPanel' as PanelKind;
it('is not reported as supported', () => {
expect(isPanelKindSupported(unknownKind)).toBe(false);
expect(isPanelKindSupported('signoz/TimeSeriesPanel')).toBe(true);
});
it('still resolves to a definition', () => {
expect(getPanelDefinition(unknownKind)).toBe(UNSUPPORTED_PANEL);
});
it('declares nothing, so it is never offered as authorable', () => {
expect(getSupportedSignals(unknownKind)).toStrictEqual([]);
expect(getSupportedQueryTypes(unknownKind)).toStrictEqual([]);
expect(isSignalSupported(unknownKind, logs)).toBe(false);
expect(
isPanelCombinationValid({ kind: unknownKind, queryType: QUERY_BUILDER }),
).toBe(false);
expect(getHiddenQueryBuilderFields(unknownKind, logs)).toStrictEqual({});
expect(getPanelDefinition(unknownKind).sections).toStrictEqual([]);
});
it('offers no actions', () => {
expect(getPanelDefinition(unknownKind).actions).toStrictEqual(
NO_PANEL_ACTIONS,
);
expect(NO_PANEL_ACTIONS.view).toBe(false);
expect(NO_PANEL_ACTIONS.edit).toBe(false);
expect(NO_PANEL_ACTIONS.drilldown).toBe(false);
});
it('carries an inert query shape, so a stray request can do no harm', () => {
const { query } = getPanelDefinition(unknownKind);
expect(query.requestType).toBe(time_series);
expect(query.serverPaginated).toBe(false);
expect(query.formatTableResultForUI).toBe(false);
});
});
describe('query type support', () => {
it.each(ALL_KINDS)('declares the expected query types for %s', (kind) => {
expect(getSupportedQueryTypes(kind)).toStrictEqual(

View File

@@ -53,10 +53,9 @@ function NoData({
return <PanelLoader />;
}
// `panelType` stays on the event so existing reports keep resolving; `panelKind` is the
// V2 identity, and the only one that can tell two kinds sharing a panel type apart.
const panelKind = panel?.spec.plugin.kind;
const panelType = panelKind ? PANEL_KIND_TO_PANEL_TYPE[panelKind] : undefined;
const panelType = panel
? PANEL_KIND_TO_PANEL_TYPE[panel.spec.plugin.kind]
: undefined;
const extendAction: PanelMessageAction | undefined =
activeExtend?.canExtend && activeExtend.actionLabel
@@ -66,7 +65,6 @@ function NoData({
void logEvent(DashboardDetailEvents.NoDataAction, {
action: 'extendTime',
panelType,
panelKind,
});
activeExtend.extend();
},
@@ -81,7 +79,6 @@ function NoData({
void logEvent(DashboardDetailEvents.NoDataAction, {
action: 'retry',
panelType,
panelKind,
});
onRetry();
},

View File

@@ -1,10 +1,7 @@
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
@@ -23,17 +20,6 @@ export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
EQueryType.PROM,
],
queryBuilderFields: {},
// Bars are binned client-side from a raw time series, so the request asks for a
// step interval wide enough to keep the bar count readable (V1 parity).
query: {
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
formatTableResultForUI: false,
bucketedStepInterval: true,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
actions: {
view: true,
edit: true,

View File

@@ -1,5 +1,6 @@
import type { DashboardtypesBarChartPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getInitialStackedBands } from 'container/DashboardContainer/visualization/charts/utils/stackSeriesUtils';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { buildBaseConfig } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/baseConfigBuilder';
@@ -47,7 +48,7 @@ export function buildBarChartConfig({
}: BuildBarChartConfigArgs): UPlotConfigBuilder {
const builder = buildBaseConfig({
panelId,
isTimeAxis: true,
panelType: PANEL_TYPES.BAR,
isDarkMode,
timezone,
panelMode,

View File

@@ -1,10 +1,7 @@
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
@@ -23,17 +20,6 @@ export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
EQueryType.PROM,
],
queryBuilderFields: {},
// Buckets are computed client-side from the raw series, so the request is a plain
// time series — the bucket count is a display concern, not a query one.
query: {
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
actions: {
view: true,
edit: true,

View File

@@ -1,5 +1,6 @@
import type { DashboardtypesHistogramPanelSpecDTO } from 'api/generated/services/sigNoz.schemas';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
import { buildBaseConfig } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/baseConfigBuilder';
import { resolveSeriesLabelV5 } from 'pages/DashboardPageV2/DashboardContainer/Panels/utils/resolveSeriesLabel';
@@ -43,7 +44,7 @@ export function buildHistogramConfig({
}: BuildHistogramConfigArgs): UPlotConfigBuilder {
const builder = buildBaseConfig({
panelId,
isTimeAxis: false,
panelType: PANEL_TYPES.HISTOGRAM,
isDarkMode,
timezone,
panelMode,

View File

@@ -1,10 +1,7 @@
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { OPERATORS } from 'constants/queryBuilder';
import { EQueryType } from 'types/common/dashboard';
@@ -33,17 +30,6 @@ export const definition: PanelDefinition<'signoz/ListPanel'> = {
},
},
sections,
// The only kind reading raw rows: they page server-side, and the sort needs a
// tiebreaker so a duplicated sort key can't repeat or skip a row across pages.
query: {
requestType: Querybuildertypesv5RequestTypeDTO.raw,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: true,
serverPaginated: true,
listView: true,
traceOperator: false,
},
actions: {
view: true,
edit: true,

View File

@@ -1,10 +1,7 @@
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/NumberPanel'> = {
@@ -23,15 +20,6 @@ export const definition: PanelDefinition<'signoz/NumberPanel'> = {
EQueryType.PROM,
],
queryBuilderFields: {},
query: {
requestType: Querybuildertypesv5RequestTypeDTO.scalar,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
actions: {
view: true,
edit: true,

View File

@@ -1,10 +1,7 @@
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
@@ -19,15 +16,6 @@ export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
],
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
queryBuilderFields: {},
query: {
requestType: Querybuildertypesv5RequestTypeDTO.scalar,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
actions: {
view: true,
edit: true,

View File

@@ -1,10 +1,7 @@
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/TablePanel'> = {
@@ -19,16 +16,6 @@ export const definition: PanelDefinition<'signoz/TablePanel'> = {
],
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
queryBuilderFields: {},
// The only kind that asks the server to transpose its scalar result into UI rows.
query: {
requestType: Querybuildertypesv5RequestTypeDTO.scalar,
formatTableResultForUI: true,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
// Tables carry tabular data worth exporting (V1 parity: download is table-only).
actions: {
view: true,

View File

@@ -1,10 +1,7 @@
import type { PanelDefinition } from '../../types/panelDefinition';
import Renderer from './Renderer';
import { sections } from './sections';
import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
@@ -23,15 +20,6 @@ export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
EQueryType.PROM,
],
queryBuilderFields: {},
query: {
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
formatTableResultForUI: false,
bucketedStepInterval: false,
orderTiebreaker: false,
serverPaginated: false,
listView: false,
traceOperator: true,
},
actions: {
view: true,
edit: true,

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