Compare commits

..

36 Commits

Author SHA1 Message Date
Nityananda Gohain
0a086ba27c Merge branch 'main' into issue_4203 2026-06-16 13:51:36 +05:30
nityanandagohain
59ca03330f fix: lint 2026-06-04 15:33:06 +05:30
nityanandagohain
c076d48b9c fix: comment 2026-06-04 15:02:48 +05:30
nityanandagohain
7f7e6e3659 Merge remote-tracking branch 'origin/issue_4203' into issue_4203 2026-06-04 14:59:38 +05:30
nityanandagohain
b210e5f532 fix: lint 2026-06-04 14:58:57 +05:30
Nityananda Gohain
8c719696bf Merge branch 'main' into issue_4203 2026-06-04 14:41:05 +05:30
nityanandagohain
7ae7c6eb4b fix: tests 2026-06-04 14:40:33 +05:30
nityanandagohain
c4efc0d2da Merge remote-tracking branch 'origin/main' into issue_4203 2026-06-04 12:52:36 +05:30
nityanandagohain
13dec174bf fix: move tests to the same file 2026-05-19 01:00:03 +05:30
nityanandagohain
9ee57c0950 fix: lint issues 2026-05-19 00:07:34 +05:30
nityanandagohain
33df48c822 fix: send all data for trace operators as well 2026-05-19 00:06:16 +05:30
nityanandagohain
af117374c8 fix: lint issues 2026-05-18 18:18:46 +05:30
nityanandagohain
ba4cef67ac fix: remove unnecessary tests 2026-05-18 17:58:09 +05:30
nityanandagohain
f0c33a6734 fix: send parsed events and links 2026-05-18 17:50:40 +05:30
nityanandagohain
e897f4866a Merge remote-tracking branch 'origin/main' into issue_4203 2026-05-18 16:30:00 +05:30
nityanandagohain
282b6fdef1 fix: address comments 2026-05-07 20:09:11 +05:30
Nityananda Gohain
9b64bb2fc0 Merge branch 'main' into issue_4203 2026-05-04 11:12:10 +05:30
nityanandagohain
b818ff5fc4 fix: address comments 2026-04-29 17:19:19 +05:30
nityanandagohain
e7d729ab5d Merge remote-tracking branch 'origin/main' into issue_4203 2026-04-29 16:51:49 +05:30
Nityananda Gohain
ed812ad1c8 Merge branch 'main' into issue_4203 2026-04-24 11:25:38 +05:30
nityanandagohain
3b82c2ce43 fix: restrict merging to only span data 2026-04-24 11:25:11 +05:30
nityanandagohain
214980ddad Merge remote-tracking branch 'origin/main' into issue_4203 2026-04-24 10:22:33 +05:30
nityanandagohain
a7b69a2678 fix: py-fmt 2026-04-21 12:13:47 +05:30
nityanandagohain
73c82f50a9 Merge remote-tracking branch 'origin/main' into issue_4203 2026-04-21 11:49:52 +05:30
nityanandagohain
2593c5eb91 fix: linting issues 2026-04-13 15:44:43 +05:30
Nityananda Gohain
b6b2d36baa Merge branch 'main' into issue_4203 2026-04-10 17:15:08 +05:30
nityanandagohain
a444a039f9 Merge remote-tracking branch 'origin/issue_4203' into issue_4203 2026-04-10 17:13:22 +05:30
nityanandagohain
bfb050ec17 fix: add changes 2026-04-10 16:57:50 +05:30
nityanandagohain
ff3e87f70c Merge remote-tracking branch 'origin/main' into issue_4203 2026-04-09 21:29:11 +05:30
Nityananda Gohain
9ac02ebe00 Merge branch 'main' into issue_4203 2026-03-25 15:50:04 +05:30
nityanandagohain
fbdd0bebbc Merge remote-tracking branch 'origin/main' into issue_4203 2026-03-25 15:21:52 +05:30
nityanandagohain
b2245b48fe fix: retain existing behaviour 2026-03-23 11:03:34 +05:30
Nityananda Gohain
87e654fc73 chore: add comment
Co-authored-by: Tushar Vats <tushar@signoz.io>
2026-03-18 16:54:09 +05:30
nityanandagohain
0ee31ce440 chore: fix tests 2026-03-17 18:16:51 +05:30
nityanandagohain
63e681b87b chore: add integration tests 2026-03-17 15:38:00 +05:30
nityanandagohain
28375c8c1e chore: send all data for trace list api 2026-03-13 19:31:59 +05:30
30 changed files with 871 additions and 1990 deletions

View File

@@ -363,6 +363,15 @@ func (q *builderQuery[T]) executeWithContext(ctx context.Context, query string,
return nil, err
}
// TODO: This should move to readAsRaw function in consume.go but for now we are keeping it here since it's only relevant for traces
if q.spec.Signal == telemetrytypes.SignalTraces {
if raw, ok := payload.(*qbtypes.RawData); ok {
for _, rr := range raw.Rows {
mergeSpanAttributeColumns(rr.Data)
}
}
}
return &qbtypes.Result{
Type: q.kind,
Value: payload,

View File

@@ -14,6 +14,7 @@ import (
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
"github.com/SigNoz/signoz/pkg/errors"
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/spantypes"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/bytedance/sonic"
)
@@ -452,6 +453,53 @@ func readAsRaw(rows driver.Rows, queryName string) (*qbtypes.RawData, error) {
}, nil
}
// mergeSpanAttributeColumns merges (attributes_string, attributes_number, attributes_bool, resources_string) into
// unified "attributes" and "resource" keys, and parses the stringified `events`
// and `links` columns into structured slices. Raw DB columns are removed.
func mergeSpanAttributeColumns(data map[string]any) {
attrStr, hasStr := data["attributes_string"]
attrNum, hasNum := data["attributes_number"]
attrBool, hasBool := data["attributes_bool"]
// todo(nitya): move to resource json
resStr, hasRes := data["resources_string"]
if hasStr || hasNum || hasBool || hasRes {
attributes := make(map[string]any)
if m, ok := attrStr.(map[string]string); ok {
for k, v := range m {
attributes[k] = v
}
}
if m, ok := attrNum.(map[string]float64); ok {
for k, v := range m {
attributes[k] = v
}
}
if m, ok := attrBool.(map[string]bool); ok {
for k, v := range m {
attributes[k] = v
}
}
delete(data, "attributes_string")
delete(data, "attributes_number")
delete(data, "attributes_bool")
data["attributes"] = attributes
resource := map[string]string{}
if m, ok := resStr.(map[string]string); ok {
resource = m
}
data["resource"] = resource
delete(data, "resources_string")
}
if raw, ok := data["events"]; ok {
data["events"] = spantypes.ParseEvents(raw)
}
if raw, ok := data["links"]; ok {
data["links"] = spantypes.ParseLinks(raw)
}
}
// numericAsFloat converts numeric types to float64 efficiently.
func numericAsFloat(v any) float64 {
switch x := v.(type) {

View File

@@ -0,0 +1,92 @@
package querier
import (
"reflect"
"testing"
"github.com/SigNoz/signoz/pkg/types/spantypes"
)
func TestMergeSpanAttributeColumns_ParsesEventsAndLinks(t *testing.T) {
data := map[string]any{
"attributes_string": map[string]string{"http.method": "GET"},
"attributes_number": map[string]float64{"http.status_code": 200},
"attributes_bool": map[string]bool{"is_root": true},
"resources_string": map[string]string{"service.name": "api"},
"events": []string{
`{"name":"request_received","timeUnixNano":1778489782759245000,"attributeMap":{"http.method":"GET","http.route":"/api/chat"}}`,
`{"name":"cache_lookup","timeUnixNano":1778489782811697000,"attributeMap":{"cache.hit":"true","cache.key":"user:123:prompt"}}`,
},
"links": `[{"traceId":"abc","spanId":"123","refType":"CHILD_OF"},{"traceId":"def","spanId":"456","refType":"FOLLOWS_FROM"}]`,
}
mergeSpanAttributeColumns(data)
attrs, ok := data["attributes"].(map[string]any)
if !ok {
t.Fatalf("expected attributes to be map[string]any, got %T", data["attributes"])
}
if attrs["http.method"] != "GET" || attrs["http.status_code"] != float64(200) || attrs["is_root"] != true {
t.Fatalf("attributes not merged correctly: %#v", attrs)
}
res, ok := data["resource"].(map[string]string)
if !ok || res["service.name"] != "api" {
t.Fatalf("resource not set correctly: %#v", data["resource"])
}
for _, removed := range []string{"attributes_string", "attributes_number", "attributes_bool", "resources_string"} {
if _, present := data[removed]; present {
t.Fatalf("expected %s to be removed", removed)
}
}
events, ok := data["events"].([]spantypes.EventV2)
if !ok {
t.Fatalf("expected events to be []spantypes.EventV2, got %T", data["events"])
}
wantEvents := []spantypes.EventV2{
{
Name: "request_received",
TimeUnixNano: 1778489782759245000,
Attributes: map[string]any{"http.method": "GET", "http.route": "/api/chat"},
IsError: false,
},
{
Name: "cache_lookup",
TimeUnixNano: 1778489782811697000,
Attributes: map[string]any{"cache.hit": "true", "cache.key": "user:123:prompt"},
},
}
if !reflect.DeepEqual(events, wantEvents) {
t.Fatalf("events parsed incorrectly:\n got: %#v\nwant: %#v", events, wantEvents)
}
links, ok := data["links"].([]spantypes.Link)
if !ok {
t.Fatalf("expected links to be []spantypes.Link, got %T", data["links"])
}
wantLinks := []spantypes.Link{
{TraceID: "abc", SpanID: "123"},
{TraceID: "def", SpanID: "456"},
}
if !reflect.DeepEqual(links, wantLinks) {
t.Fatalf("links parsed incorrectly:\n got: %#v\nwant: %#v", links, wantLinks)
}
}
func TestMergeSpanAttributeColumns_EmptyEventsAndLinks(t *testing.T) {
data := map[string]any{
"events": []string{},
"links": "[]",
}
mergeSpanAttributeColumns(data)
if events, ok := data["events"].([]spantypes.EventV2); !ok || len(events) != 0 {
t.Fatalf("expected empty []spantypes.EventV2, got %#v", data["events"])
}
if links, ok := data["links"].([]spantypes.Link); !ok || len(links) != 0 {
t.Fatalf("expected empty []spantypes.Link, got %#v", data["links"])
}
}

View File

@@ -85,6 +85,13 @@ func (q *traceOperatorQuery) executeWithContext(ctx context.Context, query strin
return nil, err
}
// TODO: This should move to readAsRaw function in consume.go but for now we can keep it here since it's only relevant for traces
if raw, ok := payload.(*qbtypes.RawData); ok {
for _, rr := range raw.Rows {
mergeSpanAttributeColumns(rr.Data)
}
}
return &qbtypes.Result{
Type: q.kind,
Value: payload,

View File

@@ -4,6 +4,48 @@ import (
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
)
const (
// Internal Columns.
SpanTimestampBucketStartColumn = "ts_bucket_start"
SpanResourceFingerPrintColumn = "resource_fingerprint"
// Intrinsic Columns.
SpanTimestampColumn = "timestamp"
SpanTraceIDColumn = "trace_id"
SpanSpanIDColumn = "span_id"
SpanTraceStateColumn = "trace_state"
SpanParentSpanIDColumn = "parent_span_id"
SpanFlagsColumn = "flags"
SpanNameColumn = "name"
SpanKindColumn = "kind"
SpanKindStringColumn = "kind_string"
SpanDurationNanoColumn = "duration_nano"
SpanStatusCodeColumn = "status_code"
SpanStatusMessageColumn = "status_message"
SpanStatusCodeStringColumn = "status_code_string"
SpanEventsColumn = "events"
SpanLinksColumn = "links"
// Calculated Columns.
SpanResponseStatusCodeColumn = "response_status_code"
SpanExternalHTTPURLColumn = "external_http_url"
SpanHTTPURLColumn = "http_url"
SpanExternalHTTPMethodColumn = "external_http_method"
SpanHTTPMethodColumn = "http_method"
SpanHTTPHostColumn = "http_host"
SpanDBNameColumn = "db_name"
SpanDBOperationColumn = "db_operation"
SpanHasErrorColumn = "has_error"
SpanIsRemoteColumn = "is_remote"
// Contextual Columns.
SpanAttributesStringColumn = "attributes_string"
SpanAttributesNumberColumn = "attributes_number"
SpanAttributesBoolColumn = "attributes_bool"
SpanResourcesStringColumn = "resources_string"
)
var (
IntrinsicFields = map[string]telemetrytypes.TelemetryFieldKey{
"trace_id": {
@@ -336,6 +378,51 @@ var (
SpanSearchScopeRoot = "isroot"
SpanSearchScopeEntryPoint = "isentrypoint"
// IntrinsicSpanFields lists the intrinsic span columns, in the order they
// should appear when a raw query expands its SelectFields.
IntrinsicSpanFields = []telemetrytypes.TelemetryFieldKey{
{Name: SpanTimestampColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanTraceIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanSpanIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanTraceStateColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanParentSpanIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanFlagsColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanNameColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanKindColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanKindStringColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanDurationNanoColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanStatusCodeColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanStatusMessageColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanStatusCodeStringColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanEventsColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanLinksColumn, FieldContext: telemetrytypes.FieldContextSpan},
}
// CalculatedSpanFields lists the calculated/derived span columns, in the
// order they should appear when a raw query expands its SelectFields.
CalculatedSpanFields = []telemetrytypes.TelemetryFieldKey{
{Name: SpanResponseStatusCodeColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanExternalHTTPURLColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanHTTPURLColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanExternalHTTPMethodColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanHTTPMethodColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanHTTPHostColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanDBNameColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanDBOperationColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanHasErrorColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanIsRemoteColumn, FieldContext: telemetrytypes.FieldContextSpan},
}
// ContextualSpanColumns lists the typed attribute and resource columns
// selected raw (rather than via ColumnExpressionFor) so that consume.go
// can merge them into unified "attributes" and "resource" maps.
ContextualSpanColumns = []string{
SpanAttributesStringColumn,
SpanAttributesNumberColumn,
SpanAttributesBoolColumn,
SpanResourcesStringColumn,
}
DefaultFields = map[string]telemetrytypes.TelemetryFieldKey{
"timestamp": {
Name: "timestamp",

View File

@@ -82,6 +82,17 @@ func TestGetFieldKeyName(t *testing.T) {
expectedResult: "multiIf(resource.`deployment.environment` IS NOT NULL, resource.`deployment.environment`::String, `resource_string_deployment$$environment_exists`==true, `resource_string_deployment$$environment`, NULL)",
expectedError: nil,
},
{
// Query like `attribute.attribute_string:string` should resolve to `attributes_string['attribute_string']`.
name: "Attribute key whose name collides with contextual map column resolves as a map lookup",
key: telemetrytypes.TelemetryFieldKey{
Name: SpanAttributesStringColumn,
FieldContext: telemetrytypes.FieldContextAttribute,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
expectedResult: "attributes_string['attributes_string']",
expectedError: nil,
},
{
name: "Non-existent column",
key: telemetrytypes.TelemetryFieldKey{

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"log/slog"
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
@@ -16,7 +15,6 @@ import (
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
"github.com/huandu/go-sqlbuilder"
"golang.org/x/exp/maps"
)
var (
@@ -86,40 +84,13 @@ func (b *traceQueryStatementBuilder) Build(
start = querybuilder.ToNanoSecs(start)
end = querybuilder.ToNanoSecs(end)
/*
Adding a tech debt note here:
This piece of code is a hot fix and should be removed once we close issue: engineering-pod/issues/3622
*/
/*
-------------------------------- Start of tech debt ----------------------------
*/
isSelectFieldsEmpty := false
if requestType == qbtypes.RequestTypeRaw {
selectedFields := query.SelectFields
if len(selectedFields) == 0 {
sortedKeys := maps.Keys(DefaultFields)
slices.Sort(sortedKeys)
for _, key := range sortedKeys {
selectedFields = append(selectedFields, DefaultFields[key])
}
query.SelectFields = selectedFields
}
selectFieldKeys := []string{}
for _, field := range selectedFields {
selectFieldKeys = append(selectFieldKeys, field.Name)
}
for _, x := range []string{"timestamp", "span_id", "trace_id"} {
if !slices.Contains(selectFieldKeys, x) {
query.SelectFields = append(query.SelectFields, DefaultFields[x])
}
}
isSelectFieldsEmpty = len(query.SelectFields) == 0
// we are expanding here to ensure that all the conflicts are taken care in adjustKeys
// i.e if there is a conflict we strip away context of the key in adjustKeys
query = b.expandRawSelectFields(query)
}
/*
-------------------------------- End of tech debt ----------------------------
*/
// We modify SelectFields above (injecting default fields), and those default
// fields can carry keys that need evolutions, so fetch keys after that.
@@ -139,7 +110,7 @@ func (b *traceQueryStatementBuilder) Build(
switch requestType {
case qbtypes.RequestTypeRaw:
return b.buildListQuery(ctx, q, query, start, end, keys, variables)
return b.buildListQuery(ctx, q, query, start, end, keys, variables, isSelectFieldsEmpty)
case qbtypes.RequestTypeTimeSeries:
return b.buildTimeSeriesQuery(ctx, q, query, start, end, keys, variables)
case qbtypes.RequestTypeScalar:
@@ -305,6 +276,7 @@ func (b *traceQueryStatementBuilder) buildListQuery(
start, end uint64,
keys map[string][]*telemetrytypes.TelemetryFieldKey,
variables map[string]qbtypes.VariableItem,
isSelectFieldsEmpty bool,
) (*qbtypes.Statement, error) {
var (
@@ -321,7 +293,6 @@ func (b *traceQueryStatementBuilder) buildListQuery(
cteArgs = append(cteArgs, args)
}
// TODO: should we deprecate `SelectFields` and return everything from a span like we do for logs?
for _, field := range query.SelectFields {
colExpr, err := b.fm.ColumnExpressionFor(ctx, start, end, &field, keys)
if err != nil {
@@ -330,6 +301,12 @@ func (b *traceQueryStatementBuilder) buildListQuery(
sb.SelectMore(colExpr)
}
if isSelectFieldsEmpty {
for _, col := range ContextualSpanColumns {
sb.SelectMore(col)
}
}
// From table
sb.From(fmt.Sprintf("%s.%s", DBName, SpanIndexV3TableName))
@@ -851,3 +828,30 @@ func (b *traceQueryStatementBuilder) maybeAttachResourceFilter(
sb.Where("resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter)")
return fmt.Sprintf("__resource_filter AS (%s)", stmt.Query), stmt.Args, true, nil
}
// expandRawSelectFields populates SelectFields for raw (list view) queries.
// It must be called before adjustKeys so that normalization runs over the full set.
func (b *traceQueryStatementBuilder) expandRawSelectFields(query qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]) qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation] {
if len(query.SelectFields) == 0 {
selectFields := make([]telemetrytypes.TelemetryFieldKey, 0, len(IntrinsicSpanFields)+len(CalculatedSpanFields))
selectFields = append(selectFields, IntrinsicSpanFields...)
selectFields = append(selectFields, CalculatedSpanFields...)
query.SelectFields = selectFields
return query
}
selectFields := []telemetrytypes.TelemetryFieldKey{
{Name: SpanTimestampColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanTraceIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
{Name: SpanSpanIDColumn, FieldContext: telemetrytypes.FieldContextSpan},
}
for _, field := range query.SelectFields {
// TODO(tvats): If a user specifies attribute.timestamp in the select fields, this loop will basically ignore it, as we already added a field by default. This can be fixed once we close https://github.com/SigNoz/engineering-pod/issues/3693
if field.Name == SpanTimestampColumn || field.Name == SpanTraceIDColumn || field.Name == SpanSpanIDColumn {
continue
}
selectFields = append(selectFields, field)
}
query.SelectFields = selectFields
return query
}

View File

@@ -462,7 +462,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT name AS `name`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, duration_nano AS `duration_nano`, `attribute_number_cart$$items_count` AS `cart.items_count`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, duration_nano AS `duration_nano`, `attribute_number_cart$$items_count` AS `cart.items_count` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -491,7 +491,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY attributes_string['user.id'] AS `user.id` desc LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, trace_state AS `trace_state`, parent_span_id AS `parent_span_id`, flags AS `flags`, name AS `name`, kind AS `kind`, kind_string AS `kind_string`, duration_nano AS `duration_nano`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY attributes_string['user.id'] AS `user.id` desc LIMIT ?",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -535,7 +535,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, response_status_code AS `responseStatusCode`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, response_status_code AS `responseStatusCode` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -579,7 +579,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, multiIf(toString(`attribute_string_mixed$$materialization$$key`) != '', toString(`attribute_string_mixed$$materialization$$key`), toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)) != '', toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)), NULL) AS `mixed.materialization.key`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, multiIf(toString(`attribute_string_mixed$$materialization$$key`) != '', toString(`attribute_string_mixed$$materialization$$key`), toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)) != '', toString(multiIf(resource.`mixed.materialization.key` IS NOT NULL, resource.`mixed.materialization.key`::String, mapContains(resources_string, 'mixed.materialization.key'), resources_string['mixed.materialization.key'], NULL)), NULL) AS `mixed.materialization.key` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -624,7 +624,7 @@ func TestStatementBuilderListQuery(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, `attribute_string_mixed$$materialization$$key` AS `mixed.materialization.key`, timestamp AS `timestamp`, span_id AS `span_id`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, name AS `name`, resource_string_service$$name AS `serviceName`, duration_nano AS `durationNano`, http_method AS `httpMethod`, `attribute_string_mixed$$materialization$$key` AS `mixed.materialization.key` FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"redis-manual", "%service.name%", "%service.name\":\"redis-manual%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -746,7 +746,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
Limit: 10,
},
expected: qbtypes.Statement{
Query: "SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Query: "SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, trace_state AS `trace_state`, parent_span_id AS `parent_span_id`, flags AS `flags`, name AS `name`, kind AS `kind`, kind_string AS `kind_string`, duration_nano AS `duration_nano`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? LIMIT ?",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -788,7 +788,7 @@ func TestStatementBuilderListQueryWithCorruptData(t *testing.T) {
}},
},
expected: qbtypes.Statement{
Query: "SELECT duration_nano AS `duration_nano`, name AS `name`, response_status_code AS `response_status_code`, multiIf(resource.`service.name` IS NOT NULL, resource.`service.name`::String, mapContains(resources_string, 'service.name'), resources_string['service.name'], NULL) AS `service.name`, span_id AS `span_id`, timestamp AS `timestamp`, trace_id AS `trace_id` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp AS `timestamp` asc LIMIT ?",
Query: "SELECT timestamp AS `timestamp`, trace_id AS `trace_id`, span_id AS `span_id`, trace_state AS `trace_state`, parent_span_id AS `parent_span_id`, flags AS `flags`, name AS `name`, kind AS `kind`, kind_string AS `kind_string`, duration_nano AS `duration_nano`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? ORDER BY timestamp AS `timestamp` asc LIMIT ?",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,

View File

@@ -424,6 +424,20 @@ func (b *traceOperatorCTEBuilder) buildNotCTE(leftCTE, rightCTE string) (string,
}
func (b *traceOperatorCTEBuilder) buildFinalQuery(ctx context.Context, selectFromCTE string, requestType qbtypes.RequestType) (*qbtypes.Statement, error) {
// Mirror statement_builder.go::Build: for raw queries, empty selectFields
// expands to the full intrinsic + calculated set, and the list query also
// pulls in the contextual columns so the consume layer can merge them
// into unified attributes/resource (and parse events/links).
isSelectFieldsEmpty := false
if requestType == qbtypes.RequestTypeRaw {
isSelectFieldsEmpty = len(b.operator.SelectFields) == 0
if isSelectFieldsEmpty {
b.operator.SelectFields = make([]telemetrytypes.TelemetryFieldKey, 0, len(IntrinsicSpanFields)+len(CalculatedSpanFields))
b.operator.SelectFields = append(b.operator.SelectFields, IntrinsicSpanFields...)
b.operator.SelectFields = append(b.operator.SelectFields, CalculatedSpanFields...)
}
}
keySelectors := b.getKeySelectors()
keys, _, err := b.stmtBuilder.metadataStore.GetKeysMulti(ctx, keySelectors)
if err != nil {
@@ -433,7 +447,7 @@ func (b *traceOperatorCTEBuilder) buildFinalQuery(ctx context.Context, selectFro
switch requestType {
case qbtypes.RequestTypeRaw:
return b.buildListQuery(ctx, selectFromCTE, keys)
return b.buildListQuery(ctx, selectFromCTE, keys, isSelectFieldsEmpty)
case qbtypes.RequestTypeTimeSeries:
return b.buildTimeSeriesQuery(ctx, selectFromCTE, keys)
case qbtypes.RequestTypeTrace:
@@ -445,10 +459,11 @@ func (b *traceOperatorCTEBuilder) buildFinalQuery(ctx context.Context, selectFro
}
}
func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFromCTE string, keys map[string][]*telemetrytypes.TelemetryFieldKey) (*qbtypes.Statement, error) {
func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFromCTE string, keys map[string][]*telemetrytypes.TelemetryFieldKey, isSelectFieldsEmpty bool) (*qbtypes.Statement, error) {
sb := sqlbuilder.NewSelectBuilder()
// Select core fields
// Select core fields. These are always present so the trace operator
// response shape is stable regardless of user-supplied selectFields.
sb.Select(
"timestamp",
"trace_id",
@@ -482,6 +497,12 @@ func (b *traceOperatorCTEBuilder) buildListQuery(ctx context.Context, selectFrom
selectedFields[field.Name] = true
}
if isSelectFieldsEmpty {
for _, col := range ContextualSpanColumns {
sb.SelectMore(col)
}
}
sb.From(selectFromCTE)
// Add order by support using ColumnExpressionFor

View File

@@ -123,7 +123,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `trace_state`, flags AS `flags`, kind AS `kind`, kind_string AS `kind_string`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_INDIR_DESC_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
},
expectedErr: nil,
@@ -160,7 +160,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_AND_B AS (SELECT l.* FROM A AS l INNER JOIN B AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `trace_state`, flags AS `flags`, kind AS `kind`, kind_string AS `kind_string`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_AND_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 15},
},
expectedErr: nil,
@@ -197,7 +197,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_OR_B AS (SELECT * FROM A UNION DISTINCT SELECT * FROM B) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `trace_state`, flags AS `flags`, kind AS `kind`, kind_string AS `kind_string`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_OR_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 20},
},
expectedErr: nil,
@@ -234,7 +234,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_not_B AS (SELECT l.* FROM A AS l WHERE l.trace_id GLOBAL NOT IN (SELECT DISTINCT trace_id FROM B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `trace_state`, flags AS `flags`, kind AS `kind`, kind_string AS `kind_string`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_not_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -399,7 +399,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_DIR_DESC_B AS (SELECT p.* FROM A AS p INNER JOIN B AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_D AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), D AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_D) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), C_DIR_DESC_D AS (SELECT p.* FROM C AS p INNER JOIN D AS c ON p.trace_id = c.trace_id AND p.span_id = c.parent_span_id), A_DIR_DESC_B_AND_C_DIR_DESC_D AS (SELECT l.* FROM A_DIR_DESC_B AS l INNER JOIN C_DIR_DESC_D AS r ON l.trace_id = r.trace_id) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `trace_state`, flags AS `flags`, kind AS `kind`, kind_string AS `kind_string`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM A_DIR_DESC_B_AND_C_DIR_DESC_D ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "frontend", "%service.name%", "%service.name\":\"frontend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "backend", "%service.name%", "%service.name\":\"backend%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 5},
},
expectedErr: nil,
@@ -433,7 +433,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __return_from_B AS (SELECT * FROM B WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM __return_from_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __return_from_B AS (SELECT * FROM B WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `trace_state`, flags AS `flags`, kind AS `kind`, kind_string AS `kind_string`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM __return_from_B ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,
@@ -475,7 +475,7 @@ func TestTraceOperatorStatementBuilder(t *testing.T) {
},
},
expected: qbtypes.Statement{
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B_AND_C AS (SELECT l.* FROM A_INDIR_DESC_B AS l INNER JOIN C AS r ON l.trace_id = r.trace_id), __return_from_C AS (SELECT * FROM C WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id FROM __return_from_C ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Query: "WITH toDateTime64(1747947419000000000, 9) AS t_from, toDateTime64(1747983448000000000, 9) AS t_to, 1747945619 AS bucket_from, 1747983448 AS bucket_to, all_spans AS (SELECT *, resource_string_service$$name AS `service.name` FROM signoz_traces.distributed_signoz_index_v3 WHERE timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_A AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), A AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_A) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), __resource_filter_B AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), B AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_B) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B AS (WITH RECURSIVE up AS (SELECT d.trace_id, d.span_id, d.parent_span_id, 0 AS depth FROM B AS d UNION ALL SELECT p.trace_id, p.span_id, p.parent_span_id, up.depth + 1 FROM all_spans AS p JOIN up ON p.trace_id = up.trace_id AND p.span_id = up.parent_span_id WHERE up.depth < 100) SELECT DISTINCT a.* FROM A AS a GLOBAL INNER JOIN (SELECT DISTINCT trace_id, span_id FROM up WHERE depth > 0 ) AS ancestors ON ancestors.trace_id = a.trace_id AND ancestors.span_id = a.span_id), __resource_filter_C AS (SELECT fingerprint FROM signoz_traces.distributed_traces_v3_resource WHERE (simpleJSONExtractString(labels, 'service.name') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint), C AS (SELECT * FROM signoz_traces.distributed_signoz_index_v3 WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter_C) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ?), A_INDIR_DESC_B_AND_C AS (SELECT l.* FROM A_INDIR_DESC_B AS l INNER JOIN C AS r ON l.trace_id = r.trace_id), __return_from_C AS (SELECT * FROM C WHERE trace_id IN (SELECT DISTINCT trace_id FROM A_INDIR_DESC_B_AND_C)) SELECT timestamp, trace_id, span_id, name, duration_nano, parent_span_id, trace_state AS `trace_state`, flags AS `flags`, kind AS `kind`, kind_string AS `kind_string`, status_code AS `status_code`, status_message AS `status_message`, status_code_string AS `status_code_string`, events AS `events`, links AS `links`, response_status_code AS `response_status_code`, external_http_url AS `external_http_url`, http_url AS `http_url`, external_http_method AS `external_http_method`, http_method AS `http_method`, http_host AS `http_host`, db_name AS `db_name`, db_operation AS `db_operation`, has_error AS `has_error`, is_remote AS `is_remote`, attributes_string, attributes_number, attributes_bool, resources_string FROM __return_from_C ORDER BY timestamp DESC LIMIT ? SETTINGS distributed_product_mode='allow', max_memory_usage=10000000000",
Args: []any{"1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "gateway", "%service.name%", "%service.name\":\"gateway%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "database", "%service.name%", "%service.name\":\"database%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), "auth", "%service.name%", "%service.name\":\"auth%", uint64(1747945619), uint64(1747983448), "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10},
},
expectedErr: nil,

View File

@@ -236,6 +236,7 @@ type RawStream struct {
Error chan error
}
func roundToNonZeroDecimals(val float64, n int) float64 {
if val == 0 || math.IsNaN(val) || math.IsInf(val, 0) {
return val

View File

@@ -0,0 +1,54 @@
package spantypes
import "encoding/json"
// The Event struct has the data exactly store in the db, while EventV2 is more of what we want to send to client.
type EventV2 struct {
Name string `json:"name"`
TimeUnixNano uint64 `json:"timeUnixNano"`
Attributes map[string]any `json:"attributes,omitempty"`
IsError bool `json:"isError,omitempty"`
}
// Link is the response shape for a span link.
// The refType field is intentionally not decoded; it's a Jaeger-era
// concept that OTel doesn't model, so we drop it on the way out.
type Link struct {
TraceID string `json:"traceId,omitempty"`
SpanID string `json:"spanId,omitempty"`
}
// ParseEvents column (Array(String) of JSON-encoded events) into a slice of Event values.
// Malformed entries are skipped.
func ParseEvents(raw any) []EventV2 {
strs, ok := raw.([]string)
if !ok {
return []EventV2{}
}
events := make([]EventV2, 0, len(strs))
for _, s := range strs {
var e Event
if err := json.Unmarshal([]byte(s), &e); err != nil {
continue
}
events = append(events, EventV2{
Name: e.Name,
TimeUnixNano: e.TimeUnixNano,
Attributes: e.AttributeMap,
IsError: false,
})
}
return events
}
func ParseLinks(raw any) []Link {
s, ok := raw.(string)
if !ok || s == "" {
return []Link{}
}
var links []Link
if err := json.Unmarshal([]byte(s), &links); err != nil {
return []Link{}
}
return links
}

View File

@@ -32,6 +32,7 @@ func (p *PostableWaterfall) Validate() error {
}
// Event represents a span event.
// todo():depricate this and use EventV2 everywhere instead while sending to client.
type Event struct {
Name string `json:"name,omitempty"`
TimeUnixNano uint64 `json:"timeUnixNano,omitempty"`

View File

@@ -6,13 +6,6 @@ import {
type Page,
} from '@playwright/test';
import {
detectPersona,
detectSettingsEnv,
type Persona,
type SettingsEnv,
} from '../helpers/persona';
export type User = { email: string; password: string };
// Default user — admin from the pytest bootstrap (.env.local) or staging .env.
@@ -27,11 +20,6 @@ export const ADMIN: User = {
type StorageState = Awaited<ReturnType<BrowserContext['storageState']>>;
const storageByUser = new Map<string, Promise<StorageState>>();
// Per-worker persona/env caches by user email. Detection is constant for a
// given backend + user, so it runs once per worker.
const personaByUser = new Map<string, Promise<Persona>>();
const envByUser = new Map<string, Promise<SettingsEnv>>();
async function storageFor(browser: Browser, user: User): Promise<StorageState> {
const cached = storageByUser.get(user.email);
if (cached) {
@@ -84,10 +72,6 @@ export const test = base.extend<{
* storageState is held in memory and reused for all later requests.
*/
authedPage: Page;
persona: Persona;
env: SettingsEnv;
}>({
user: [ADMIN, { option: true }],
@@ -109,24 +93,6 @@ export const test = base.extend<{
await use(page);
await ctx.close();
},
persona: async ({ authedPage, user }, use) => {
let task = personaByUser.get(user.email);
if (!task) {
task = detectPersona(authedPage);
personaByUser.set(user.email, task);
}
await use(await task);
},
env: async ({ authedPage, user }, use) => {
let task = envByUser.get(user.email);
if (!task) {
task = detectSettingsEnv(authedPage);
envByUser.set(user.email, task);
}
await use(await task);
},
});
export { expect };

View File

@@ -1,124 +0,0 @@
import type { Page } from '@playwright/test';
import { authToken } from './dashboards';
export type Tier =
| 'cloud'
| 'enterprise'
| 'community'
| 'community-enterprise';
export type Role = 'ADMIN' | 'EDITOR' | 'VIEWER' | 'ANONYMOUS';
export interface Persona {
tier: Tier;
role: Role;
}
export interface SettingsEnv {
isGatewayEnabled: boolean;
}
interface AuthzCheckItem {
authorized?: boolean;
object?: { selector?: string };
}
interface FeatureFlag {
name?: string;
active?: boolean;
}
const LICENSE_URL = '/api/v3/licenses/active';
const AUTHZ_CHECK_URL = '/api/v1/authz/check';
const FEATURES_URL = '/api/v1/features';
// Mirrors IsAdmin/Editor/Viewer in frontend/src/hooks/useAuthZ/legacy.ts:
// relation 'assignee' on resource kind/type 'role', selector = preset role id.
const ROLE_PROBES: { role: Exclude<Role, 'ANONYMOUS'>; selector: string }[] = [
{ role: 'ADMIN', selector: 'signoz-admin' },
{ role: 'EDITOR', selector: 'signoz-editor' },
{ role: 'VIEWER', selector: 'signoz-viewer' },
];
function authHeaders(token: string): Record<string, string> {
return { Authorization: `Bearer ${token}` };
}
function parseOverride(): Persona | null {
const raw = process.env.SIGNOZ_E2E_PERSONA;
if (!raw) {
return null;
}
const parts = raw.toLowerCase().split('-');
const roleRaw = parts.pop();
const tier = parts.join('-') as Tier;
const role = roleRaw?.toUpperCase() as Role;
return { tier, role };
}
async function detectTier(page: Page, token: string): Promise<Tier> {
const res = await page.request.get(LICENSE_URL, {
headers: authHeaders(token),
});
if (res.status() === 404) {
return 'community-enterprise';
}
if (res.status() === 501) {
return 'community';
}
const body = await res.json();
const platform = body?.data?.platform;
if (platform === 'CLOUD') {
return 'cloud';
}
return 'enterprise';
}
async function detectRole(page: Page, token: string): Promise<Role> {
const payload = ROLE_PROBES.map((p) => ({
relation: 'assignee',
object: {
resource: { kind: 'role', type: 'role' },
selector: p.selector,
},
}));
const res = await page.request.post(AUTHZ_CHECK_URL, {
headers: authHeaders(token),
data: payload,
});
const body = await res.json();
const items: AuthzCheckItem[] = body?.data ?? [];
const granted = new Set(
items.filter((i) => i?.authorized).map((i) => i?.object?.selector),
);
for (const p of ROLE_PROBES) {
if (granted.has(p.selector)) {
return p.role;
}
}
return 'ANONYMOUS';
}
export async function detectPersona(page: Page): Promise<Persona> {
const override = parseOverride();
if (override) {
return override;
}
const token = await authToken(page);
const [tier, role] = await Promise.all([
detectTier(page, token),
detectRole(page, token),
]);
return { tier, role };
}
export async function detectSettingsEnv(page: Page): Promise<SettingsEnv> {
const token = await authToken(page);
const res = await page.request.get(FEATURES_URL, {
headers: authHeaders(token),
});
const body = await res.json();
const flags: FeatureFlag[] = body?.data ?? [];
const gateway = flags.find((f) => f?.name === 'gateway');
return { isGatewayEnabled: !!gateway?.active };
}

View File

@@ -1,52 +0,0 @@
import type { Page } from '@playwright/test';
import { expect } from '../fixtures/auth';
// Verbatim from frontend/src/constants/routes.ts
export const SETTINGS_ROUTES = {
WORKSPACE: '/settings',
MY_SETTINGS: '/settings/my-settings',
ORG_SETTINGS: '/settings/org-settings',
ALL_CHANNELS: '/settings/channels',
INGESTION: '/settings/ingestion-settings',
BILLING: '/settings/billing',
ROLES: '/settings/roles',
MEMBERS: '/settings/members',
SERVICE_ACCOUNTS: '/settings/service-accounts',
SHORTCUTS: '/settings/shortcuts',
MCP_SERVER: '/settings/mcp-server',
INTEGRATIONS: '/integrations',
} as const;
export type SettingsRoute =
(typeof SETTINGS_ROUTES)[keyof typeof SETTINGS_ROUTES];
// Sidenav item data-testid == itemKey in menuItems.tsx settingsNavSections.
export const NAV_TESTID: Record<string, string> = {
[SETTINGS_ROUTES.WORKSPACE]: 'workspace',
[SETTINGS_ROUTES.MY_SETTINGS]: 'account',
[SETTINGS_ROUTES.ALL_CHANNELS]: 'notification-channels',
[SETTINGS_ROUTES.BILLING]: 'billing',
[SETTINGS_ROUTES.INTEGRATIONS]: 'integrations',
[SETTINGS_ROUTES.MCP_SERVER]: 'mcp-server',
[SETTINGS_ROUTES.ROLES]: 'roles',
[SETTINGS_ROUTES.MEMBERS]: 'members',
[SETTINGS_ROUTES.SERVICE_ACCOUNTS]: 'service-accounts',
[SETTINGS_ROUTES.INGESTION]: 'ingestion',
[SETTINGS_ROUTES.ORG_SETTINGS]: 'sso',
[SETTINGS_ROUTES.SHORTCUTS]: 'keyboard-shortcuts',
};
export async function gotoSettings(page: Page): Promise<void> {
await page.goto(SETTINGS_ROUTES.WORKSPACE);
await expect(page.getByTestId('settings-page-title')).toBeVisible();
}
export async function openSettingsTab(
page: Page,
route: SettingsRoute,
): Promise<void> {
const testid = NAV_TESTID[route];
await page.getByTestId('settings-page-sidenav').getByTestId(testid).click();
await expect(page).toHaveURL(new RegExp(route.replace(/\//g, '\\/')));
}

View File

@@ -1,156 +0,0 @@
import type { Persona, SettingsEnv, Tier } from './persona';
import { SETTINGS_ROUTES, NAV_TESTID } from './settings';
// Mirrors the isEnabled effect in frontend/src/pages/Settings/Settings.tsx.
// Returns the set of sidenav item testids (itemKeys) that should be visible.
export function visibleNavItems(
persona: Persona,
_env: SettingsEnv,
): Set<string> {
const { tier, role } = persona;
const isAdmin = role === 'ADMIN';
const isEditor = role === 'EDITOR';
const isViewer = role === 'VIEWER';
// Defaults that start enabled in menuItems.tsx settingsNavSections.
const s = new Set<string>([
'workspace',
'account',
'notification-channels',
'keyboard-shortcuts',
]);
const enableForAllUsers = (): void => {
s.add('roles');
s.add('service-accounts');
};
if (tier === 'cloud') {
enableForAllUsers();
if (isAdmin) {
[
'billing',
'integrations',
'ingestion',
'sso',
'members',
'mcp-server',
].forEach((k) => s.add(k));
}
if (isEditor) {
['ingestion', 'integrations', 'mcp-server'].forEach((k) => s.add(k));
}
if (isViewer) {
s.add('mcp-server');
}
return s;
}
if (tier === 'enterprise') {
enableForAllUsers();
if (isAdmin) {
[
'billing',
'integrations',
'sso',
'members',
'ingestion',
'mcp-server',
].forEach((k) => s.add(k));
}
if (isEditor) {
['integrations', 'ingestion', 'mcp-server'].forEach((k) => s.add(k));
}
if (isViewer) {
s.add('mcp-server');
}
return s;
}
// community / community-enterprise (!cloud && !enterprise)
enableForAllUsers();
if (isAdmin) {
s.add('sso');
s.add('members');
}
// billing & integrations explicitly disabled for non-cloud users.
s.delete('billing');
s.delete('integrations');
return s;
}
// Mirrors getRoutes() in frontend/src/pages/Settings/utils.ts.
// Returns the set of /settings route paths that are mounted (navigable).
export function registeredRoutes(
persona: Persona,
env: SettingsEnv,
): Set<string> {
const { tier, role } = persona;
const isAdmin = role === 'ADMIN';
const isEditor = role === 'EDITOR';
const isCloud = tier === 'cloud';
const isEnterprise = tier === 'enterprise';
const r = new Set<string>([
SETTINGS_ROUTES.WORKSPACE, // generalSettings — always
SETTINGS_ROUTES.ALL_CHANNELS, // always
SETTINGS_ROUTES.SERVICE_ACCOUNTS, // always
SETTINGS_ROUTES.ROLES, // always
SETTINGS_ROUTES.MY_SETTINGS, // always
SETTINGS_ROUTES.SHORTCUTS, // always
SETTINGS_ROUTES.MCP_SERVER, // always
]);
// organizationSettings — gated by current_org_settings; mirrored as admin-only.
if (isAdmin) {
r.add(SETTINGS_ROUTES.ORG_SETTINGS);
}
// multiIngestionSettings if gateway && (admin||editor); cloud read-only if cloud && !gateway.
if (
(env.isGatewayEnabled && (isAdmin || isEditor)) ||
(isCloud && !env.isGatewayEnabled)
) {
r.add(SETTINGS_ROUTES.INGESTION);
}
// membersSettings if admin.
if (isAdmin) {
r.add(SETTINGS_ROUTES.MEMBERS);
}
// billing if (cloud||enterprise) && admin.
if ((isCloud || isEnterprise) && isAdmin) {
r.add(SETTINGS_ROUTES.BILLING);
}
return r;
}
// Skip reason when a route's nav item is hidden for the persona; null when
// visible. Centralised so every skip reads identically and is greppable.
export function personaSkipReason(
persona: Persona,
env: SettingsEnv,
route: string,
): string | null {
const visible = visibleNavItems(persona, env);
const testid = NAV_TESTID[route];
if (testid && visible.has(testid)) {
return null;
}
return `PERSONA_SKIP: ${route} hidden for ${persona.tier}×${persona.role}`;
}
// Second skip axis: a route is visible but renders tier-specific CONTENT (e.g.
// /settings shows a cloud support card vs self-hosted retention controls).
// Gates a test to the tiers whose content it asserts. Shares the PERSONA_SKIP:
// prefix.
export function tierSkipReason(
persona: Persona,
allowedTiers: Tier[],
label: string,
): string | null {
if (allowedTiers.includes(persona.tier)) {
return null;
}
return `PERSONA_SKIP: ${label} not applicable for tier ${persona.tier} (needs ${allowedTiers.join(
'|',
)})`;
}

View File

@@ -1,151 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '../../fixtures/auth';
import {
personaSkipReason,
tierSkipReason,
} from '../../helpers/settingsAccess';
import { SETTINGS_ROUTES } from '../../helpers/settings';
// Workspace (/settings) has two views: cloud (retention inputs disabled, no Save,
// GeneralSettingsCloud support card) and self-hosted (interactive inputs, per-row Save).
// Retention inputs in compact mode have no data-testid — role/text/CSS fallback.
async function gotoWorkspace(page: Page): Promise<void> {
await page.goto(SETTINGS_ROUTES.WORKSPACE);
// Retention data is fetched server-side; allow margin for the API response.
await expect(page.locator('.retention-controls-container')).toBeVisible({
timeout: 15_000,
});
}
function retentionRow(page: Page, signal: string) {
return page.locator('.retention-row').filter({ hasText: signal });
}
function retentionInput(page: Page, signal: string) {
return retentionRow(page, signal).locator('input[type="number"]').first();
}
function saveButton(page: Page, signal: string) {
return retentionRow(page, signal).getByRole('button', { name: /^save$/i });
}
// Tier sets for the two Workspace content variants.
const CLOUD_TIERS = ['cloud'] as const;
const SELF_HOSTED_TIERS = [
'enterprise',
'community',
'community-enterprise',
] as const;
test.describe('Settings — Workspace / General page', () => {
test('TC-01 page renders retention controls and license-key row', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.WORKSPACE),
personaSkipReason(persona, env, SETTINGS_ROUTES.WORKSPACE) ?? undefined,
);
await gotoWorkspace(page);
// Scoped to avoid strict-mode conflict with the sidenav item.
await expect(page.locator('.general-settings-title')).toContainText(
'Workspace',
);
await expect(page.locator('.general-settings-subtitle')).toContainText(
'Manage your workspace settings.',
);
await expect(page.getByText('Retention Controls')).toBeVisible();
await expect(retentionRow(page, 'Metrics')).toBeVisible();
await expect(retentionRow(page, 'Traces')).toBeVisible();
await expect(retentionRow(page, 'Logs')).toBeVisible();
await expect(retentionInput(page, 'Metrics')).toBeVisible();
await expect(retentionInput(page, 'Traces')).toBeVisible();
await expect(retentionInput(page, 'Logs')).toBeVisible();
await expect(page.getByTestId('license-key-row-copy-btn')).toBeVisible();
});
// RISK MODE: read-only — only asserts disabled state, nothing is mutated.
test('TC-02 cloud view — retention inputs are disabled and support card is visible', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.WORKSPACE),
personaSkipReason(persona, env, SETTINGS_ROUTES.WORKSPACE) ?? undefined,
);
test.skip(
!!tierSkipReason(persona, [...CLOUD_TIERS], 'cloud retention view'),
tierSkipReason(persona, [...CLOUD_TIERS], 'cloud retention view') ??
undefined,
);
await gotoWorkspace(page);
await expect(retentionInput(page, 'Metrics')).toBeDisabled();
await expect(retentionInput(page, 'Traces')).toBeDisabled();
await expect(retentionInput(page, 'Logs')).toBeDisabled();
await expect(saveButton(page, 'Metrics')).toHaveCount(0);
await expect(saveButton(page, 'Traces')).toHaveCount(0);
await expect(saveButton(page, 'Logs')).toHaveCount(0);
await expect(
page.getByText(/please.*email us.*or connect.*via chat support/i),
).toBeVisible();
});
// RISK MODE: never clicks Save — only asserts enable-on-change / disable-on-clear; no PUT/POST.
test('TC-03 self-hosted view — retention input enables/disables Save — no save triggered', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.WORKSPACE),
personaSkipReason(persona, env, SETTINGS_ROUTES.WORKSPACE) ?? undefined,
);
test.skip(
!!tierSkipReason(
persona,
[...SELF_HOSTED_TIERS],
'self-hosted retention controls',
),
tierSkipReason(
persona,
[...SELF_HOSTED_TIERS],
'self-hosted retention controls',
) ?? undefined,
);
await gotoWorkspace(page);
const metricsInput = retentionInput(page, 'Metrics');
const metricsSaveBtn = saveButton(page, 'Metrics');
const originalValue = await metricsInput.inputValue();
try {
await metricsInput.fill('9999');
await expect(metricsSaveBtn).toBeEnabled();
await metricsInput.fill('');
await expect(metricsSaveBtn).toBeDisabled();
await expect(
page.getByText(/retention period for .+ is not set yet/i),
).toBeVisible();
} finally {
// Restore so unsaved UI state does not leak to other workers sharing this stack.
await metricsInput.fill(originalValue);
}
});
});

View File

@@ -1,117 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '../../fixtures/auth';
import {
personaSkipReason,
tierSkipReason,
} from '../../helpers/settingsAccess';
import { SETTINGS_ROUTES } from '../../helpers/settings';
// Ingestion page, two variants gated by env.isGatewayEnabled / tier:
// MultiIngestionSettings (gateway ON) vs read-only IngestionSettings (cloud, gateway OFF).
// RISK MODE — READ-ONLY: never create/edit/delete keys or rate limits; create
// button and copy affordances asserted for presence only, never clicked.
// Each TC guards its variant via test.skip so bodies stay branch-free
// (playwright/no-conditional-in-test).
test.describe.configure({ mode: 'serial' });
async function gotoIngestion(page: Page): Promise<void> {
await page.goto(SETTINGS_ROUTES.INGESTION);
// Ingestion keys/settings are fetched server-side; allow margin for the API response.
await expect(
page
.locator('.ingestion-key-container, .ingestion-settings-container')
.first(),
).toBeVisible({ timeout: 15_000 });
}
test.describe('Settings — Ingestion page', () => {
test('TC-01 MultiIngestionSettings — page chrome, search, table, and create affordance render', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.INGESTION),
personaSkipReason(persona, env, SETTINGS_ROUTES.INGESTION) ?? undefined,
);
test.skip(
!!tierSkipReason(
persona,
['cloud', 'enterprise'],
'MultiIngestionSettings (gateway)',
) || !env.isGatewayEnabled,
!env.isGatewayEnabled
? 'PERSONA_SKIP: gateway feature flag is OFF — MultiIngestionSettings does not render'
: (tierSkipReason(
persona,
['cloud', 'enterprise'],
'MultiIngestionSettings (gateway)',
) ?? undefined),
);
await gotoIngestion(page);
const container = page.locator('.ingestion-key-container');
await expect(container).toBeVisible();
// Exact name match avoids the subtitle partial match.
await expect(
container.getByRole('heading', { name: 'Ingestion Keys' }),
).toBeVisible();
await expect(
container.getByText(/Create and manage ingestion keys/i),
).toBeVisible();
await expect(
container.getByPlaceholder('Search for ingestion key...'),
).toBeVisible();
await expect(
container.getByRole('button', { name: /new ingestion key/i }),
).toBeVisible();
await expect(container.locator('.ingestion-keys-table')).toBeVisible();
await expect(
container.locator('.ingestion-key-url-label', { hasText: 'Ingestion URL' }),
).toBeVisible();
});
test('TC-02 IngestionSettings (read-only) — table rows for URL, key, and region render', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.INGESTION),
personaSkipReason(persona, env, SETTINGS_ROUTES.INGESTION) ?? undefined,
);
// This view only renders on cloud when gateway is disabled
test.skip(
env.isGatewayEnabled,
'PERSONA_SKIP: gateway is ON — MultiIngestionSettings renders instead of read-only table',
);
test.skip(
!!tierSkipReason(persona, ['cloud'], 'IngestionSettings read-only table'),
tierSkipReason(persona, ['cloud'], 'IngestionSettings read-only table') ??
undefined,
);
await gotoIngestion(page);
const container = page.locator('.ingestion-settings-container');
await expect(container).toBeVisible();
await expect(
container.getByText(/start sending your telemetry data/i),
).toBeVisible();
const table = container.locator('.ant-table');
await expect(table).toBeVisible();
await expect(table.getByText('Ingestion URL')).toBeVisible();
await expect(table.getByText('Ingestion Key')).toBeVisible();
await expect(table.getByText('Ingestion Region')).toBeVisible();
});
});

View File

@@ -1,153 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '../../fixtures/auth';
import { newAdminContext } from '../../helpers/auth';
import { authToken } from '../../helpers/dashboards';
import { personaSkipReason } from '../../helpers/settingsAccess';
import { SETTINGS_ROUTES } from '../../helpers/settings';
// MCP Server settings, two variants gated by mcp_url in /api/v1/global/config:
// full page (mcp_url present, cloud) vs NotCloudFallback (absent, community/self-hosted).
// RISK MODE — READ-ONLY: never create a service account; copy/create/install
// buttons asserted for presence only, never clicked.
// mcpEndpointPresent is probed in beforeAll (real backend state) so TC-01/TC-02
// skip via test.skip rather than branching in bodies (playwright/no-conditional-in-test).
test.describe.configure({ mode: 'serial' });
let mcpEndpointPresent = false;
test.beforeAll(async ({ browser }) => {
const ctx = await newAdminContext(browser);
const page = await ctx.newPage();
try {
const token = await authToken(page);
const res = await page.request.get('/api/v1/global/config', {
headers: { Authorization: `Bearer ${token}` },
});
if (res.ok()) {
const body = await res.json();
const mcpUrl: unknown = body?.data?.mcp_url;
mcpEndpointPresent = typeof mcpUrl === 'string' && mcpUrl.length > 0;
}
} finally {
await ctx.close();
}
});
async function gotoMcpServer(page: Page): Promise<void> {
await page.goto(SETTINGS_ROUTES.MCP_SERVER);
// Spinner gone => either full page or fallback has rendered.
await expect(page.locator('.ant-spin-spinning')).toHaveCount(0);
}
test.describe('Settings — MCP Server page', () => {
// Locators below use CSS classes / role-text; only mcp-settings has a data-testid.
test('TC-01 full page renders: header, client tabs, auth card, use-cases card', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MCP_SERVER),
personaSkipReason(persona, env, SETTINGS_ROUTES.MCP_SERVER) ?? undefined,
);
// Full-page content requires mcp_url to be configured. If not present the
// NotCloudFallback renders instead — TC-02 covers that path.
test.skip(
!mcpEndpointPresent,
'PERSONA_SKIP: mcp_url not configured on this stack — NotCloudFallback renders; see TC-02',
);
await gotoMcpServer(page);
await expect(page.getByTestId('mcp-settings')).toBeVisible();
await expect(page.locator('.mcp-settings__header-title')).toContainText(
'SigNoz MCP Server',
);
await expect(page.locator('.mcp-settings__header-subtitle')).toContainText(
'Model Context Protocol',
);
await expect(page.locator('.mcp-settings__card')).toBeVisible();
await expect(page.locator('.mcp-settings__card-title')).toContainText(
'Configure your client',
);
const tabsRoot = page.locator('.mcp-client-tabs-root');
await expect(tabsRoot).toBeVisible();
await expect(tabsRoot.getByRole('tab', { name: /cursor/i })).toBeVisible();
await expect(
tabsRoot.getByRole('tab', { name: /claude code/i }),
).toBeVisible();
await expect(tabsRoot.getByRole('tab', { name: /vs code/i })).toBeVisible();
await expect(
page.locator('.mcp-client-tabs__snippet-pre').first(),
).toBeVisible();
await expect(
page.getByRole('button', { name: /copy cursor config/i }),
).toBeVisible();
const authCard = page.locator('.mcp-auth-card');
await expect(authCard).toBeVisible();
await expect(authCard.locator('.mcp-auth-card__title')).toContainText(
'Authenticate from your client',
);
await expect(
authCard.locator('.mcp-auth-card__field-label').first(),
).toContainText('SigNoz Instance URL');
await expect(
authCard.getByRole('button', { name: /copy signoz instance url/i }),
).toBeVisible();
await expect(
authCard.locator('.mcp-auth-card__field-label').nth(1),
).toContainText('API Key');
await expect(
authCard.getByRole('button', { name: /create service account/i }),
).toBeVisible();
const useCasesCard = page.locator('.mcp-use-cases-card');
await expect(useCasesCard).toBeVisible();
await expect(
useCasesCard.locator('.mcp-use-cases-card__title'),
).toContainText('What you can do with it');
await expect(useCasesCard.locator('.mcp-use-cases-card__list')).toBeVisible();
await expect(
useCasesCard.getByRole('button', { name: /see more use cases/i }),
).toBeVisible();
});
// Skipped when the beforeAll probe found mcp_url — full page renders instead.
test('TC-02 NotCloudFallback renders when MCP endpoint is not configured', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MCP_SERVER),
personaSkipReason(persona, env, SETTINGS_ROUTES.MCP_SERVER) ?? undefined,
);
test.skip(
mcpEndpointPresent,
'PERSONA_SKIP: mcp_url is configured on this stack — NotCloudFallback does not render',
);
await gotoMcpServer(page);
await expect(page.locator('.not-cloud-fallback')).toBeVisible();
await expect(page.locator('.not-cloud-fallback__title')).toContainText(
'MCP Server is available on SigNoz',
);
await expect(
page.getByRole('button', { name: /view mcp server docs/i }),
).toBeVisible();
await expect(page.getByTestId('mcp-settings')).toHaveCount(0);
});
});

View File

@@ -1,205 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '../../fixtures/auth';
import { personaSkipReason } from '../../helpers/settingsAccess';
import { SETTINGS_ROUTES } from '../../helpers/settings';
// RISK MODE: read-only plus one non-submitting invite-modal check — no member is
// created/edited/deleted/role-changed. The fresh bootstrap stack has exactly one
// member (seeded admin, active), so filter/search coverage is limited to that row.
// No data-testid exists in MembersSettings/Table/InviteModal — role/placeholder/text/CSS fallback.
test.describe.configure({ mode: 'serial' });
const ADMIN_EMAIL = process.env.SIGNOZ_E2E_USERNAME ?? 'admin@integration.test';
const SEARCH_PLACEHOLDER = 'Search by name or email...';
async function gotoMembers(page: Page): Promise<void> {
await page.goto(SETTINGS_ROUTES.MEMBERS);
// Members list is fetched server-side; allow margin for the API response.
await expect(page.locator('.members-table-wrapper')).toBeVisible({
timeout: 15_000,
});
}
test.describe('Settings — Members page', () => {
test('TC-01 list renders with columns and the bootstrap admin user row', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MEMBERS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MEMBERS) ?? undefined,
);
await gotoMembers(page);
await expect(
page.getByRole('heading', { name: 'Members', level: 1 }),
).toBeVisible();
await expect(
page.getByText('Overview of people added to this workspace.'),
).toBeVisible();
await expect(page.locator('.members-filter-trigger')).toBeVisible();
await expect(page.getByPlaceholder(SEARCH_PLACEHOLDER)).toBeVisible();
await expect(
page.getByRole('button', { name: /invite member/i }),
).toBeVisible();
const table = page.locator('.members-table');
await expect(
table.getByRole('columnheader', { name: 'Name / Email' }),
).toBeVisible();
await expect(
table.getByRole('columnheader', { name: 'Status' }),
).toBeVisible();
await expect(
table.getByRole('columnheader', { name: 'Joined On' }),
).toBeVisible();
await expect(
page.locator('.member-email', { hasText: ADMIN_EMAIL }),
).toBeVisible();
const adminRow = page
.locator('tr')
.filter({ has: page.locator('.member-email', { hasText: ADMIN_EMAIL }) });
await expect(adminRow.getByText('ACTIVE')).toBeVisible();
});
// On the single-member stack, Pending/Deleted both yield the empty state.
test('TC-02 filter dropdown — cycles All / Pending / Deleted and updates the list', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MEMBERS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MEMBERS) ?? undefined,
);
await gotoMembers(page);
await expect(
page.locator('.member-email', { hasText: ADMIN_EMAIL }),
).toBeVisible();
await page.locator('.members-filter-trigger').click();
const menu = page.getByRole('menu');
await expect(menu).toBeVisible();
await menu.getByText(/pending invites/i).click();
await expect(page.locator('.members-empty-state')).toBeVisible();
await expect(
page.locator('.member-email', { hasText: ADMIN_EMAIL }),
).toHaveCount(0);
await page.locator('.members-filter-trigger').click();
await expect(page.getByRole('menu')).toBeVisible();
await page
.getByRole('menu')
.getByText(/^deleted/i)
.click();
await expect(page.locator('.members-empty-state')).toBeVisible();
await expect(
page.locator('.member-email', { hasText: ADMIN_EMAIL }),
).toHaveCount(0);
await page.locator('.members-filter-trigger').click();
await expect(page.getByRole('menu')).toBeVisible();
await page
.getByRole('menu')
.getByText(/all members/i)
.click();
await expect(
page.locator('.member-email', { hasText: ADMIN_EMAIL }),
).toBeVisible();
await expect(page.locator('.members-empty-state')).toHaveCount(0);
});
test('TC-03 search filters by email match and shows empty state on no match', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MEMBERS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MEMBERS) ?? undefined,
);
await gotoMembers(page);
const searchInput = page.getByPlaceholder(SEARCH_PLACEHOLDER);
await searchInput.fill(ADMIN_EMAIL);
await expect(
page.locator('.member-email', { hasText: ADMIN_EMAIL }),
).toBeVisible();
await expect(page.locator('.members-empty-state')).toHaveCount(0);
await searchInput.fill('xyznonexistentuser999@nowhere.invalid');
await expect(page.locator('.members-empty-state')).toBeVisible();
await expect(
page
.locator('.members-empty-state__text')
.getByText('xyznonexistentuser999@nowhere.invalid'),
).toBeVisible();
await expect(
page.locator('.member-email', { hasText: ADMIN_EMAIL }),
).toHaveCount(0);
await searchInput.fill('');
await expect(
page.locator('.member-email', { hasText: ADMIN_EMAIL }),
).toBeVisible();
await expect(page.locator('.members-empty-state')).toHaveCount(0);
});
// RISK MODE: submit is never clicked; no invite is sent.
test('TC-04 invite modal — renders correctly, submit disabled on untouched rows, Cancel dismisses', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MEMBERS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MEMBERS) ?? undefined,
);
await gotoMembers(page);
await page.getByRole('button', { name: /invite member/i }).click();
const modal = page.getByRole('dialog');
await expect(modal).toBeVisible();
await expect(
modal.getByRole('heading', { name: 'Invite Team Members' }),
).toBeVisible();
// Header cells scoped to class selectors to avoid matching input placeholders.
await expect(modal.locator('.email-header')).toBeVisible();
await expect(modal.locator('.role-header')).toBeVisible();
// Modal starts with 3 empty rows.
const emailInputs = modal.locator('input[type="email"]');
await expect(emailInputs.first()).toBeVisible();
await expect(emailInputs).toHaveCount(3);
await expect(
modal.getByRole('button', { name: /add another/i }),
).toBeVisible();
// Submit is disabled while all rows are untouched.
const submitBtn = modal.getByRole('button', { name: 'Invite Team Members' });
await expect(submitBtn).toBeVisible();
await expect(submitBtn).toBeDisabled();
await modal.getByRole('button', { name: /cancel/i }).click();
await expect(modal).not.toBeVisible();
});
});

View File

@@ -1,262 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '../../fixtures/auth';
import { authToken } from '../../helpers/dashboards';
import { personaSkipReason } from '../../helpers/settingsAccess';
import { SETTINGS_ROUTES } from '../../helpers/settings';
test.describe.configure({ mode: 'serial' });
// Runtime branching lives in these helpers, not test() bodies — playwright/no-conditional-in-test.
async function gotoMySettings(page: Page): Promise<void> {
await page.goto(SETTINGS_ROUTES.MY_SETTINGS);
await expect(page.getByTestId('theme-selector')).toBeVisible();
}
async function readThemeState(
page: Page,
): Promise<{ theme: string; autoSwitch: string }> {
// globalThis cast: the evaluate callback runs in the browser, but the e2e
// tsconfig uses the ES2020 lib (no DOM), so `localStorage` isn't typed here.
return page.evaluate(() => ({
theme: (globalThis as any).localStorage.getItem('THEME') ?? 'dark',
autoSwitch:
(globalThis as any).localStorage.getItem('THEME_AUTO_SWITCH') ?? 'false',
}));
}
async function restoreTheme(
page: Page,
theme: string,
autoSwitch: string,
): Promise<void> {
await page.evaluate(
([t, a]) => {
(globalThis as any).localStorage.setItem('THEME', t);
(globalThis as any).localStorage.setItem('THEME_AUTO_SWITCH', a);
},
[theme, autoSwitch],
);
}
async function restoreSideNavPinned(
page: Page,
originalChecked: string,
): Promise<void> {
const token = await authToken(page);
await page.request.put('/api/v1/user/preferences/sidenav_pinned', {
data: { value: originalChecked === 'true' },
headers: { Authorization: `Bearer ${token}` },
});
}
function flipAriaChecked(current: string): string {
if (current === 'true') {
return 'false';
}
return 'true';
}
test.describe('My Settings — Account page', () => {
test('TC-01 page renders with all expected controls', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS) ?? undefined,
);
await gotoMySettings(page);
await expect(
page.getByRole('button', { name: /update name/i }),
).toBeVisible();
await expect(
page.getByRole('button', { name: /reset password/i }).first(),
).toBeVisible();
await expect(page.getByTestId('theme-selector')).toBeVisible();
await expect(page.getByTestId('timezone-adaptation-switch')).toBeVisible();
await expect(page.getByTestId('side-nav-pinned-switch')).toBeVisible();
// License copy button renders because bootstrap issues an enterprise license on cloud.
await expect(page.getByTestId('license-key-copy-btn')).toBeVisible();
});
test('TC-02 theme toggle cycles dark → light → auto and applies', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS) ?? undefined,
);
await gotoMySettings(page);
const originalTheme = await readThemeState(page);
try {
// Radix ToggleGroup renders items as role="radio" within a radiogroup.
const selector = page.getByTestId('theme-selector');
const darkRadio = selector.getByRole('radio', { name: /dark/i });
const lightRadio = selector.getByRole('radio', { name: /light/i });
const systemRadio = selector.getByRole('radio', { name: /system/i });
await lightRadio.click();
await expect(lightRadio).toBeChecked();
await systemRadio.click();
await expect(systemRadio).toBeChecked();
await darkRadio.click();
await expect(darkRadio).toBeChecked();
} finally {
await restoreTheme(page, originalTheme.theme, originalTheme.autoSwitch);
}
});
test('TC-03 sidebar pin toggle flips checked state', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS) ?? undefined,
);
await gotoMySettings(page);
const switchEl = page.getByTestId('side-nav-pinned-switch');
const originalChecked =
(await switchEl.getAttribute('aria-checked')) ?? 'false';
const expectedAfterToggle = flipAriaChecked(originalChecked);
try {
await switchEl.click();
// Pin state persists server-side; allow margin for the update under
// parallel-worker CPU contention (default 5s expect timeout flakes).
await expect(switchEl).toHaveAttribute('aria-checked', expectedAfterToggle, {
timeout: 15_000,
});
} finally {
await restoreSideNavPinned(page, originalChecked);
}
});
test('TC-04 timezone adaptation toggle flips checked state', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS) ?? undefined,
);
await gotoMySettings(page);
const switchEl = page.getByTestId('timezone-adaptation-switch');
const originalChecked =
(await switchEl.getAttribute('aria-checked')) ?? 'true';
const expectedAfterToggle = flipAriaChecked(originalChecked);
try {
await switchEl.click();
await expect(switchEl).toHaveAttribute('aria-checked', expectedAfterToggle, {
timeout: 15_000,
});
} finally {
// isAdaptationEnabled is not persisted — toggle back to restore session state.
await switchEl.click();
}
});
// note: PUT /api/v2/users/me returns root_user_operation_unsupported for the
// bootstrap admin user. Only the modal open/input/submit-button UI is tested
// here; the "name reflects in card after save" assertion cannot be verified
// against this stack.
test('TC-05 update name modal — opens, pre-fills, submit button active', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS) ?? undefined,
);
await gotoMySettings(page);
const currentName = await page.locator('.user-name').first().innerText();
await page.getByRole('button', { name: /update name/i }).click();
const nameInput = page.getByPlaceholder('e.g. John Doe');
await expect(nameInput).toBeVisible();
await expect(nameInput).toHaveValue(currentName);
const submitBtn = page.getByTestId('update-name-btn');
await expect(submitBtn).toBeVisible();
await expect(submitBtn).toBeEnabled();
// Close via × button — Ant Modal's Escape handler can race with input focus in headless mode.
await page
.locator('.update-name-modal')
.getByRole('button', { name: 'Close' })
.click();
await expect(nameInput).not.toBeVisible();
});
test('TC-06 reset-password modal — validation only, never submits', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS),
personaSkipReason(persona, env, SETTINGS_ROUTES.MY_SETTINGS) ?? undefined,
);
await gotoMySettings(page);
// The button that OPENS the modal has no testid; reset-password-btn is the SUBMIT button inside.
await page
.getByRole('button', { name: /reset password/i })
.first()
.click();
const currentPasswordInput = page.getByTestId('current-password-textbox');
const newPasswordInput = page.getByTestId('new-password-textbox');
const submitBtn = page.getByTestId('reset-password-btn');
await expect(currentPasswordInput).toBeVisible();
await expect(newPasswordInput).toBeVisible();
await expect(submitBtn).toBeDisabled();
await currentPasswordInput.fill('somepassword');
await expect(submitBtn).toBeDisabled();
// Same value → passwords match → validation error + disabled
await newPasswordInput.fill('somepassword');
await expect(page.getByText(/new password must be different/i)).toBeVisible();
await expect(submitBtn).toBeDisabled();
// Stop at enabled — clicking would rotate the admin password and break every other worker.
await newPasswordInput.fill('differentpassword!1');
await expect(submitBtn).toBeEnabled();
await page
.locator('.reset-password-modal')
.getByRole('button', { name: 'Close' })
.click();
await expect(currentPasswordInput).not.toBeVisible();
});
});

View File

@@ -1,106 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '../../fixtures/auth';
import { personaSkipReason } from '../../helpers/settingsAccess';
import { SETTINGS_ROUTES } from '../../helpers/settings';
// OrganizationSettings (/settings/org-settings): DisplayName form + AuthDomain section.
// Invite coverage lives in members.spec.ts — the #invite-team-members hash is ignored here.
//
// note: PUT /api/v2/orgs returns root_user_operation_unsupported for the bootstrap
// admin user. TC-02 only asserts the field is editable and the Submit button enables;
// it does NOT submit the form. The original org name is never mutated.
test.describe.configure({ mode: 'serial' });
async function gotoOrgSettings(page: Page): Promise<void> {
await page.goto(SETTINGS_ROUTES.ORG_SETTINGS);
await expect(page.getByLabel('Display name')).toBeVisible();
}
test.describe('Organization Settings — SSO & Org page', () => {
test('TC-01 page renders display-name field and authenticated-domains section', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.ORG_SETTINGS),
personaSkipReason(persona, env, SETTINGS_ROUTES.ORG_SETTINGS) ?? undefined,
);
await gotoOrgSettings(page);
await expect(page.getByLabel('Display name')).toBeVisible();
await expect(page.getByRole('button', { name: 'Submit' })).toBeVisible();
await expect(
page.getByRole('heading', { name: 'Authenticated Domains' }),
).toBeVisible();
await expect(page.getByRole('button', { name: 'Add Domain' })).toBeVisible();
});
// note: root_user_operation_unsupported on save (see header) — never clicks Submit; value restored in finally.
test('TC-02 org display name — field is editable and Submit enables on change', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.ORG_SETTINGS),
personaSkipReason(persona, env, SETTINGS_ROUTES.ORG_SETTINGS) ?? undefined,
);
await gotoOrgSettings(page);
const nameInput = page.getByLabel('Display name');
const submitBtn = page.getByRole('button', { name: 'Submit' });
const originalValue = await nameInput.inputValue();
try {
// Submit is disabled when the value equals the current saved name.
await expect(submitBtn).toBeDisabled();
await nameInput.fill('org-sso-spec-temp');
await expect(nameInput).toHaveValue('org-sso-spec-temp');
await expect(submitBtn).toBeEnabled();
await nameInput.fill('');
await expect(submitBtn).toBeDisabled();
} finally {
// Restored value equals the saved one, so Submit stays disabled — no API call.
await nameInput.fill(originalValue);
await expect(submitBtn).toBeDisabled();
}
});
// RISK MODE: never enable SSO/SAML or click Save — that changes auth for the whole stack.
test('TC-03 SSO config — Add Domain opens provider-selector modal, close dismisses it', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.ORG_SETTINGS),
personaSkipReason(persona, env, SETTINGS_ROUTES.ORG_SETTINGS) ?? undefined,
);
await gotoOrgSettings(page);
await page.getByRole('button', { name: 'Add Domain' }).click();
const modal = page.getByRole('dialog');
await expect(modal).toBeVisible();
await expect(
modal.getByText('Configure Authentication Method'),
).toBeVisible();
await expect(modal.getByText('Google Apps Authentication')).toBeVisible();
// SAML/OIDC visibility depends on the SSO flag — only assert Google Auth, always enabled.
await modal.getByRole('button', { name: /close/i }).click();
await expect(modal).not.toBeVisible();
});
});

View File

@@ -1,172 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '../../fixtures/auth';
import { newAdminContext } from '../../helpers/auth';
import { authToken } from '../../helpers/dashboards';
import { personaSkipReason } from '../../helpers/settingsAccess';
import { SETTINGS_ROUTES } from '../../helpers/settings';
// Roles page. RISK MODE — READ-ONLY: never create/edit/delete a role; TC-03
// only views a managed role's detail page and navigates back.
// rolesEnabled probes /api/v1/features for USE_FINE_GRAINED_AUTHZ — real backend
// state, not a guess; row navigation is only wired up when it is on, so TC-03 skips otherwise.
test.describe.configure({ mode: 'serial' });
let rolesEnabled = false;
async function gotoRolesList(page: Page): Promise<void> {
await page.goto(SETTINGS_ROUTES.ROLES);
await expect(page.getByTestId('roles-settings')).toBeVisible();
}
test.beforeAll(async ({ browser }) => {
const ctx = await newAdminContext(browser);
const page = await ctx.newPage();
try {
const token = await authToken(page);
const res = await page.request.get('/api/v1/features', {
headers: { Authorization: `Bearer ${token}` },
});
const body = await res.json();
const flags: { name?: string; active?: boolean }[] = body?.data ?? [];
const flag = flags.find((f) => f?.name === 'use_fine_grained_authz');
rolesEnabled = !!flag?.active;
} finally {
await ctx.close();
}
});
test.describe('Settings — Roles page', () => {
test('TC-01 list renders with container, header, search, and managed-role rows', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.ROLES),
personaSkipReason(persona, env, SETTINGS_ROUTES.ROLES) ?? undefined,
);
await gotoRolesList(page);
await expect(page.locator('.roles-settings-header-title')).toContainText(
'Roles',
);
await expect(
page.locator('.roles-settings-header-description'),
).toContainText('Create and manage custom roles for your team.');
await expect(page.locator('input[type="search"]')).toBeVisible();
await expect(
page.locator('input[placeholder="Search for roles..."]'),
).toBeVisible();
const table = page.locator('.roles-listing-table');
await expect(table).toBeVisible();
await expect(table.locator('.roles-table-header-cell--name')).toContainText(
'Name',
);
await expect(
table.locator('.roles-table-header-cell--description'),
).toContainText('Description');
await expect(
table.locator('.roles-table-header-cell--updated-at'),
).toContainText('Updated At');
await expect(
table.locator('.roles-table-header-cell--created-at'),
).toContainText('Created At');
await expect(
table.locator('.roles-table-section-header', { hasText: 'Managed roles' }),
).toBeVisible();
await expect(table.locator('.roles-table-row').first()).toBeVisible();
});
test('TC-02 search filters roles by match and shows empty state on no match', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.ROLES),
personaSkipReason(persona, env, SETTINGS_ROUTES.ROLES) ?? undefined,
);
await gotoRolesList(page);
const searchInput = page.locator('input[placeholder="Search for roles..."]');
const table = page.locator('.roles-listing-table');
await searchInput.fill('Admin');
await expect(
table.locator('.roles-table-cell--name', { hasText: /admin/i }).first(),
).toBeVisible();
await expect(table.locator('.roles-table-empty')).toHaveCount(0);
await searchInput.fill('xyznonexistentrole999');
await expect(table.locator('.roles-table-empty')).toBeVisible();
await expect(table.locator('.roles-table-empty')).toContainText(
'No roles match your search.',
);
await expect(table.locator('.roles-table-row')).toHaveCount(0);
await searchInput.fill('');
await expect(table.locator('.roles-table-row').first()).toBeVisible();
await expect(table.locator('.roles-table-empty')).toHaveCount(0);
});
// Read-only: views a managed role, asserts no edit/delete, navigates back.
// Skipped when USE_FINE_GRAINED_AUTHZ is off — rows have no click handler.
test('TC-03 role detail page — clicking a managed role navigates to its detail view', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, SETTINGS_ROUTES.ROLES),
personaSkipReason(persona, env, SETTINGS_ROUTES.ROLES) ?? undefined,
);
test.skip(
!rolesEnabled,
'PERSONA_SKIP: USE_FINE_GRAINED_AUTHZ feature flag is off — role rows are not clickable',
);
await gotoRolesList(page);
const table = page.locator('.roles-listing-table');
const firstRow = table.locator('.roles-table-row').first();
await firstRow.scrollIntoViewIfNeeded();
await firstRow.click();
await expect(page).toHaveURL(/\/settings\/roles\/[^/]+/);
const detailPage = page.locator('.role-details-page');
await expect(detailPage).toBeVisible();
await expect(detailPage.locator('.role-details-title')).toBeVisible();
await expect(detailPage.locator('.role-details-title')).toContainText(
'Role —',
);
await expect(
detailPage.getByText(
'This is a managed role. Permissions and settings are view-only and cannot be modified.',
),
).toBeVisible();
await expect(
detailPage.getByRole('button', { name: 'Edit Role Details' }),
).toHaveCount(0);
await expect(
detailPage.locator('.role-details-section-label', {
hasText: 'Permissions',
}),
).toBeVisible();
await page.goto(SETTINGS_ROUTES.ROLES);
await expect(page.getByTestId('roles-settings')).toBeVisible();
});
});

View File

@@ -1,191 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '../../fixtures/auth';
import { newAdminContext } from '../../helpers/auth';
import { authToken } from '../../helpers/dashboards';
import { personaSkipReason } from '../../helpers/settingsAccess';
import { SETTINGS_ROUTES } from '../../helpers/settings';
// Service Accounts page. RISK MODE — READ-ONLY: never create/edit/delete an
// account or generate a token; the create modal is never opened.
// listAccessible probes the real authz/check backend state in beforeAll (when
// use_fine_grained_authz is on the admin may lack serviceaccount:list, rendering
// PermissionDeniedFullPage); the functional TCs skip when it is false.
test.describe.configure({ mode: 'serial' });
let listAccessible = false;
async function gotoServiceAccounts(page: Page): Promise<void> {
await page.goto(SETTINGS_ROUTES.SERVICE_ACCOUNTS);
await expect(page.locator('.sa-settings__title')).toBeVisible();
}
function buildSkipReason(
persona: Parameters<typeof personaSkipReason>[0],
env: Parameters<typeof personaSkipReason>[1],
): string | null {
return personaSkipReason(persona, env, SETTINGS_ROUTES.SERVICE_ACCOUNTS);
}
test.beforeAll(async ({ browser }) => {
const ctx = await newAdminContext(browser);
const page = await ctx.newPage();
try {
const token = await authToken(page);
const res = await page.request.get('/api/v1/features', {
headers: { Authorization: `Bearer ${token}` },
});
const body = await res.json();
const flags: { name?: string; active?: boolean }[] = body?.data ?? [];
const fgAuthz = flags.find((f) => f?.name === 'use_fine_grained_authz');
if (!fgAuthz?.active) {
// Without fine-grained authz the SA list is always accessible.
listAccessible = true;
return;
}
// Probe the authz check endpoint for serviceaccount:list (wildcard).
const authzRes = await page.request.post('/api/v1/authz/check', {
headers: { Authorization: `Bearer ${token}` },
data: [
{
relation: 'list',
object: {
resource: { kind: 'serviceaccount', type: 'serviceaccount' },
selector: '*',
},
},
],
});
const authzBody = await authzRes.json();
const items: { authorized?: boolean }[] = authzBody?.data ?? [];
listAccessible = items.some((i) => i?.authorized);
} finally {
await ctx.close();
}
});
test.describe('Settings — Service Accounts page', () => {
test('TC-01 page chrome and empty-state render', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!buildSkipReason(persona, env),
buildSkipReason(persona, env) ?? undefined,
);
test.skip(
!listAccessible,
'PERSONA_SKIP: serviceaccount:list permission not granted for this persona — PermissionDeniedFullPage renders instead',
);
await gotoServiceAccounts(page);
await expect(page.locator('.sa-settings__title')).toContainText(
'Service Accounts',
);
await expect(page.locator('.sa-settings__subtitle')).toContainText(
'Overview of service accounts added to this workspace.',
);
await expect(
page.locator('.sa-settings__subtitle a[href*="signoz.io/docs"]'),
).toBeVisible();
const controls = page.locator('.sa-settings__controls');
await expect(controls).toBeVisible();
await expect(
controls.getByRole('button', { name: /All accounts/i }),
).toBeVisible();
await expect(
controls.locator('input[placeholder="Search by name or email..."]'),
).toBeVisible();
await expect(
controls.getByRole('button', { name: /New Service Account/i }),
).toBeVisible();
await expect(page.locator('.sa-table-wrapper')).toBeVisible();
await expect(page.locator('.sa-empty-state')).toBeVisible();
await expect(page.locator('.sa-empty-state__text')).toContainText(
'No service accounts.',
);
});
test('TC-02 filter dropdown writes URL param and shows empty-state per mode', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!buildSkipReason(persona, env),
buildSkipReason(persona, env) ?? undefined,
);
test.skip(
!listAccessible,
'PERSONA_SKIP: serviceaccount:list permission not granted for this persona — PermissionDeniedFullPage renders instead',
);
await gotoServiceAccounts(page);
const filterTrigger = page.getByRole('button', { name: /All accounts/i });
await filterTrigger.click();
await page.getByText(/^Active ⎯/).click();
await expect(page).toHaveURL(/[?&]filter=active/);
await expect(page.locator('.sa-empty-state')).toBeVisible();
await page.getByRole('button', { name: /Active ⎯/i }).click();
await page.getByText(/^Deleted ⎯/).click();
await expect(page).toHaveURL(/[?&]filter=deleted/);
await expect(page.locator('.sa-empty-state')).toBeVisible();
await page.getByRole('button', { name: /Deleted ⎯/i }).click();
await page.getByText(/^All accounts ⎯/).click();
await expect(page).not.toHaveURL(/[?&]filter=active/);
await expect(page).not.toHaveURL(/[?&]filter=deleted/);
await expect(page.locator('.sa-empty-state')).toBeVisible();
});
test('TC-03 search updates URL and empty-state; create button enabled', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!buildSkipReason(persona, env),
buildSkipReason(persona, env) ?? undefined,
);
test.skip(
!listAccessible,
'PERSONA_SKIP: serviceaccount:list permission not granted for this persona — PermissionDeniedFullPage renders instead',
);
await gotoServiceAccounts(page);
const searchInput = page.locator(
'input[placeholder="Search by name or email..."]',
);
await searchInput.fill('xyznonexistent999');
await expect(page).toHaveURL(/[?&]search=xyznonexistent999/);
await expect(page.locator('.sa-empty-state__text')).toContainText(
'No results for',
);
await expect(page.locator('.sa-empty-state__text strong')).toContainText(
'xyznonexistent999',
);
await searchInput.fill('');
await expect(page).not.toHaveURL(/[?&]search=xyznonexistent999/);
await expect(page.locator('.sa-empty-state__text')).toContainText(
'No service accounts.',
);
const createBtn = page.getByRole('button', { name: /New Service Account/i });
await expect(createBtn).toBeVisible();
await expect(createBtn).toBeEnabled();
});
});

View File

@@ -1,125 +0,0 @@
import type { Persona, SettingsEnv } from '../../helpers/persona';
import { expect, test } from '../../fixtures/auth';
import {
registeredRoutes,
visibleNavItems,
} from '../../helpers/settingsAccess';
import {
NAV_TESTID,
SETTINGS_ROUTES,
gotoSettings,
} from '../../helpers/settings';
// Branching lives in module-level helpers, not test bodies — the repo's
// playwright/no-conditional-in-test rule forbids `if` inside `test()`.
function partitionNavTestids(
persona: Persona,
env: SettingsEnv,
): { visible: string[]; hidden: string[] } {
const all = Object.values(NAV_TESTID);
const expected = visibleNavItems(persona, env);
return {
visible: all.filter((testid) => expected.has(testid)),
hidden: all.filter((testid) => !expected.has(testid)),
};
}
// Visible nav items whose /settings route is not registered (mounted).
// INTEGRATIONS is excluded — it is a top-level page, not a RouteTab route.
function navRouteMismatches(persona: Persona, env: SettingsEnv): string[] {
const visible = visibleNavItems(persona, env);
const registered = registeredRoutes(persona, env);
const routeByTestid = Object.fromEntries(
Object.entries(NAV_TESTID).map(([route, testid]) => [testid, route]),
);
return [...visible]
.map((testid) => routeByTestid[testid])
.filter((route) => !!route && route !== SETTINGS_ROUTES.INTEGRATIONS)
.filter((route) => !registered.has(route))
.map((route) => `${route} is nav-visible but route not registered`);
}
test.describe('Settings — shell, gating matrix & integrity', () => {
test('TC-01 settings shell chrome renders with no JS pageerror', async ({
authedPage: page,
}) => {
const errors: Error[] = [];
page.on('pageerror', (err) => errors.push(err));
await gotoSettings(page);
await expect(page.getByTestId('settings-page-title')).toBeVisible();
await expect(page.getByTestId('settings-page-sidenav')).toBeVisible();
expect(errors, errors.map((e) => e.message).join('\n')).toHaveLength(0);
});
test('TC-02 sidenav shows exactly the matrix-predicted items', async ({
authedPage: page,
persona,
env,
}) => {
await gotoSettings(page);
const sidenav = page.getByTestId('settings-page-sidenav');
const { visible, hidden } = partitionNavTestids(persona, env);
for (const testid of visible) {
await expect(
sidenav.getByTestId(testid),
`${testid} should be visible`,
).toBeVisible();
}
for (const testid of hidden) {
await expect(
sidenav.getByTestId(testid),
`${testid} should be hidden`,
).toHaveCount(0);
}
});
test('TC-03 every registered route deep-links with no JS pageerror', async ({
authedPage: page,
persona,
env,
}) => {
const routes = [...registeredRoutes(persona, env)];
for (const route of routes) {
const errors: Error[] = [];
const onError = (err: Error): void => {
errors.push(err);
};
page.on('pageerror', onError);
await page.goto(route);
await expect(page.getByTestId('settings-page-title')).toBeVisible();
page.off('pageerror', onError);
expect(
errors,
`pageerror on ${route}: ${errors.map((e) => e.message).join('\n')}`,
).toHaveLength(0);
}
});
test('TC-04 every visible nav item resolves to a registered route', async ({
persona,
env,
}) => {
const mismatches = navRouteMismatches(persona, env);
expect(mismatches, mismatches.join('\n')).toHaveLength(0);
});
test('TC-05 clicking a nav item navigates and marks active', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!visibleNavItems(persona, env).has('account'),
'PERSONA_SKIP: account nav hidden',
);
await gotoSettings(page);
const sidenav = page.getByTestId('settings-page-sidenav');
await sidenav.getByTestId('account').click();
await expect(page).toHaveURL(/\/settings\/my-settings/);
});
});

View File

@@ -1,69 +0,0 @@
import type { Page } from '@playwright/test';
import { expect, test } from '../../fixtures/auth';
import { personaSkipReason } from '../../helpers/settingsAccess';
import { SETTINGS_ROUTES } from '../../helpers/settings';
// Keyboard Shortcuts — static read-only page (RISK MODE: nothing mutated).
// No testids here, so locators are CSS classes (.keyboard-shortcuts,
// .shortcut-section-heading) and role/text.
const ROUTE = SETTINGS_ROUTES.SHORTCUTS;
async function gotoShortcuts(page: Page): Promise<void> {
await page.goto(ROUTE);
await expect(page.locator('.keyboard-shortcuts')).toBeVisible();
}
test.describe('Settings — Keyboard Shortcuts page', () => {
test('TC-01 shortcuts page renders all four grouped sections with entries', async ({
authedPage: page,
persona,
env,
}) => {
test.skip(
!!personaSkipReason(persona, env, ROUTE),
personaSkipReason(persona, env, ROUTE) ?? undefined,
);
await gotoShortcuts(page);
await expect(page.getByTestId('settings-page-title')).toBeVisible();
await expect(page.getByTestId('settings-page-sidenav')).toBeVisible();
await expect(
page.getByTestId('settings-page-sidenav').getByTestId('keyboard-shortcuts'),
).toBeVisible();
const sections = page.locator('.shortcut-section-heading');
await expect(sections).toHaveCount(4);
await expect(sections.nth(0)).toHaveText('Global Shortcuts');
await expect(sections.nth(1)).toHaveText('Logs Explorer Shortcuts');
await expect(sections.nth(2)).toHaveText('Query Builder Shortcuts');
await expect(sections.nth(3)).toHaveText('Dashboard Shortcuts');
await expect(page.locator('.shortcut-section-table')).toHaveCount(4);
const firstTable = page.locator('.shortcut-section-table').first();
await expect(
firstTable.getByRole('columnheader', { name: 'Keyboard Shortcut' }),
).toBeVisible();
await expect(
firstTable.getByRole('columnheader', { name: 'Description' }),
).toBeVisible();
// "shift+d" chosen as it is stable across OS variants (no cmd/ctrl).
const globalTable = page.locator('.shortcut-section-table').nth(0);
await expect(
globalTable.getByRole('cell', { name: 'shift+d' }),
).toBeVisible();
await expect(
globalTable.getByRole('cell', { name: 'Navigate to Dashboards List' }),
).toBeVisible();
for (let i = 0; i < 4; i++) {
const table = page.locator('.shortcut-section-table').nth(i);
await expect(table.locator('tbody tr').first()).toBeVisible();
}
});
});

View File

@@ -16,6 +16,42 @@ from fixtures import types
from fixtures.fingerprint import LogsOrTracesFingerprint
from fixtures.time import parse_duration, parse_timestamp
# All keys returned by the trace list endpoint when selectFields is empty:
# every intrinsic and calculated column, plus the merged `attributes` and
# `resource` maps that wrap the contextual columns in the response layer.
ALL_SELECT_FIELDS = [
# all intrinsic columns
"timestamp",
"trace_id",
"span_id",
"trace_state",
"parent_span_id",
"flags",
"name",
"kind",
"kind_string",
"duration_nano",
"status_code",
"status_message",
"status_code_string",
"events",
"links",
# all calculated columns
"response_status_code",
"external_http_url",
"http_url",
"external_http_method",
"http_method",
"http_host",
"db_name",
"db_operation",
"has_error",
"is_remote",
# all contextual columns (merged in response layer)
"attributes",
"resource",
]
class TracesKind(Enum):
SPAN_KIND_UNSPECIFIED = 0
@@ -236,9 +272,10 @@ class Traces(ABC):
attributes_number: dict[str, np.float64]
attributes_bool: dict[str, bool]
resources_string: dict[str, str]
# Accepting parsed events and links, but will be stored as list[str], str in db
events: list[dict[str, Any]]
links: list[dict[str, Any]]
resource_json: dict[str, str]
events: list[str]
links: str
response_status_code: str
external_http_url: str
http_url: str
@@ -428,10 +465,17 @@ class Traces(ABC):
)
)
# Process events and derive error events
# Process events and derive error events. self.events holds the parsed
# response shape; np_arr() encodes back to the DB format on insert.
self.events = []
for event in events:
self.events.append(json.dumps([event.name, event.time_unix_nano, event.attribute_map]))
self.events.append(
{
"name": event.name,
"timeUnixNano": int(event.time_unix_nano),
"attributes": dict(event.attribute_map),
}
)
# Create error events for exception events (following Go exporter logic)
if event.name == "exception":
@@ -453,7 +497,26 @@ class Traces(ABC):
),
)
self.links = json.dumps([link.__dict__() for link in links_copy], separators=(",", ":"))
# self.links holds the parsed response shape (trace_id/span_id only;
# ref_type is dropped to match the API). np_arr() re-encodes for DB insert.
self.links = [{"traceId": link.trace_id, "spanId": link.span_id} for link in links_copy]
self._links_db = json.dumps(
[link.__dict__() for link in links_copy],
separators=(",", ":"),
)
# DB shape per event: {"name", "timeUnixNano", "attributeMap"}. Must match
# what the consume-layer parser in pkg/types/spantypes expects.
self._events_db = [
json.dumps(
{
"name": event.name,
"timeUnixNano": int(event.time_unix_nano),
"attributeMap": dict(event.attribute_map),
},
separators=(",", ":"),
)
for event in events
]
# Initialize resource
self.resource = []
@@ -568,8 +631,8 @@ class Traces(ABC):
self.attributes_number,
self.attributes_bool,
self.resources_string,
self.events,
self.links,
self._events_db,
self._links_db,
self.response_status_code,
self.external_http_url,
self.http_url,

View File

@@ -17,7 +17,16 @@ from fixtures.querier import (
index_series_by_label,
make_query_request,
)
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
from fixtures.traces import (
ALL_SELECT_FIELDS,
TraceIdGenerator,
Traces,
TracesEvent,
TracesKind,
TracesLink,
TracesRefType,
TracesStatusCode,
)
def test_traces_list(
@@ -473,7 +482,9 @@ def test_traces_list(
@pytest.mark.parametrize(
"payload,status_code,results",
[
# Case 1: order by timestamp field which there in attributes as well
# Case 1: order by timestamp; empty selectFields returns the full
# response shape (all intrinsic + calculated columns plus the merged
# `attributes` and `resource` maps). x[3] (topic-service) is latest.
pytest.param(
{
"type": "builder_query",
@@ -487,19 +498,42 @@ def test_traces_list(
},
HTTPStatus.OK,
lambda x: [
x[3].duration_nano,
{
**x[3].attribute_string,
**x[3].attributes_number,
**x[3].attributes_bool,
}, # attributes
x[3].db_name,
x[3].db_operation,
int(x[3].duration_nano),
x[3].events,
x[3].external_http_method,
x[3].external_http_url,
int(x[3].flags),
x[3].has_error,
x[3].http_host,
x[3].http_method,
x[3].http_url,
x[3].is_remote,
int(x[3].kind),
x[3].kind_string,
x[3].links,
x[3].name,
x[3].parent_span_id,
x[3].resources_string,
x[3].response_status_code,
x[3].service_name,
x[3].span_id,
int(x[3].status_code),
x[3].status_code_string,
x[3].status_message,
format_timestamp(x[3].timestamp),
x[3].trace_id,
x[3].trace_state,
], # type: Callable[[List[Traces]], List[Any]]
),
# Case 2: order by attribute timestamp field which is there in attributes as well
# This should break but it doesn't because attribute.timestamp gets adjusted to timestamp
# because of default trace.timestamp gets added by default and bug in field mapper picks
# instrinsic field
# Case 2: order by attribute.timestamp. The key resolves to the
# intrinsic span.timestamp column, so the latest span (x[3]) is
# returned with the same full response shape as Case 1.
pytest.param(
{
"type": "builder_query",
@@ -513,13 +547,37 @@ def test_traces_list(
},
HTTPStatus.OK,
lambda x: [
x[3].duration_nano,
{
**x[3].attribute_string,
**x[3].attributes_number,
**x[3].attributes_bool,
}, # attributes
x[3].db_name,
x[3].db_operation,
int(x[3].duration_nano),
x[3].events,
x[3].external_http_method,
x[3].external_http_url,
int(x[3].flags),
x[3].has_error,
x[3].http_host,
x[3].http_method,
x[3].http_url,
x[3].is_remote,
int(x[3].kind),
x[3].kind_string,
x[3].links,
x[3].name,
x[3].parent_span_id,
x[3].resources_string,
x[3].response_status_code,
x[3].service_name,
x[3].span_id,
int(x[3].status_code),
x[3].status_code_string,
x[3].status_message,
format_timestamp(x[3].timestamp),
x[3].trace_id,
x[3].trace_state,
], # type: Callable[[List[Traces]], List[Any]]
),
# Case 3: select timestamp with empty order by
@@ -542,7 +600,7 @@ def test_traces_list(
], # type: Callable[[List[Traces]], List[Any]]
),
# Case 4: select attribute.timestamp with empty order by
# This doesn't return any data because of where_clause using aliased timestamp
# This returns the one span which has attribute.timestamp
pytest.param(
{
"type": "builder_query",
@@ -556,7 +614,11 @@ def test_traces_list(
},
},
HTTPStatus.OK,
lambda x: [], # type: Callable[[List[Traces]], List[Any]]
lambda x: [
x[0].span_id,
format_timestamp(x[0].timestamp),
x[0].trace_id,
], # type: Callable[[List[Traces]], List[Any]]
),
# Case 5: select timestamp with timestamp order by
pytest.param(
@@ -693,6 +755,159 @@ def test_traces_list_with_corrupt_data(
assert data[key] == value
def _verify_events_links_full(rows: list[dict], traces: list[Traces]) -> None:
"""Empty-selectFields case: events/links arrive parsed into structured objects.
Every row's events/links should match the fixture's stored parsed shape
(the fixture's `.events`/`.links` mirror the API response shape directly).
"""
for row, trace in zip(rows, traces, strict=True):
assert row["data"]["events"] == trace.events
assert row["data"]["links"] == trace.links
# Jaeger-era `refType` is dropped at the consume layer.
for link in row["data"]["links"]:
assert "refType" not in link
def _verify_events_links_skip(rows: list[dict], traces: list[Traces]) -> None:
"""Projected-selectFields case: nothing to verify beyond the key set."""
@pytest.mark.parametrize(
"select_fields,status_code,expected_keys,verify_values",
[
pytest.param(
[],
HTTPStatus.OK,
ALL_SELECT_FIELDS,
_verify_events_links_full,
),
pytest.param(
[
{"name": "service.name"},
],
HTTPStatus.OK,
["timestamp", "trace_id", "span_id", "service.name"],
_verify_events_links_skip,
),
],
)
def test_traces_list_with_select_fields(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[list[Traces]], None],
select_fields: list[dict],
status_code: HTTPStatus,
expected_keys: list[str],
verify_values: Callable[[list[dict], list[Traces]], None],
) -> None:
"""
Setup:
Insert a root span with no events/links and a child span carrying two
events and one user-supplied link.
Tests:
1. Empty select fields should return all the fields, and the `events` /
`links` columns should arrive parsed into structured objects (events
carry `attributes`, links carry only `traceId`/`spanId` — refType is
dropped at the consume layer).
2. Non-empty select field should return the select field along with
timestamp, trace_id and span_id.
"""
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
parent_trace_id = TraceIdGenerator.trace_id()
parent_span_id = TraceIdGenerator.span_id()
child_span_id = TraceIdGenerator.span_id()
linked_trace_id = TraceIdGenerator.trace_id()
linked_span_id = TraceIdGenerator.span_id()
event_one = TracesEvent(
name="request_received",
timestamp=now - timedelta(seconds=3, microseconds=500_000),
attribute_map={"http.method": "GET", "http.route": "/api/chat"},
)
event_two = TracesEvent(
name="cache_lookup",
timestamp=now - timedelta(seconds=3, microseconds=400_000),
attribute_map={"cache.hit": "true", "cache.key": "user:123:prompt"},
)
user_link = TracesLink(
trace_id=linked_trace_id,
span_id=linked_span_id,
ref_type=TracesRefType.REF_TYPE_FOLLOWS_FROM,
)
traces = [
# Root span: no events, no links. Verifies the empty-case parsed shape.
Traces(
timestamp=now - timedelta(seconds=4),
duration=timedelta(seconds=3),
trace_id=parent_trace_id,
span_id=parent_span_id,
parent_span_id="",
name="root span",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
resources={"service.name": "events-links-service"},
attributes={"http.request.method": "GET"},
),
# Child span: two events + one user-supplied link. The fixture
# auto-inserts a CHILD_OF link for the parent, so the parsed response
# contains two links total — the auto-inserted one first.
Traces(
timestamp=now - timedelta(seconds=3),
duration=timedelta(seconds=1),
trace_id=parent_trace_id,
span_id=child_span_id,
parent_span_id=parent_span_id,
name="child span",
kind=TracesKind.SPAN_KIND_INTERNAL,
status_code=TracesStatusCode.STATUS_CODE_OK,
resources={"service.name": "events-links-service"},
attributes={"http.request.method": "GET"},
events=[event_one, event_two],
links=[user_link],
),
]
insert_traces(traces)
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
payload = {
"type": "builder_query",
"spec": {
"name": "A",
"signal": "traces",
"filter": {"expression": "resource.service.name = 'events-links-service'"},
"selectFields": select_fields,
"order": [{"key": {"name": "timestamp"}, "direction": "asc"}],
"limit": 10,
},
}
response = make_query_request(
signoz,
token,
start_ms=int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000),
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
request_type="raw",
queries=[payload],
)
assert response.status_code == status_code
if response.status_code != HTTPStatus.OK:
return
rows = response.json()["data"]["data"]["results"][0]["rows"]
assert len(rows) == 2
for row in rows:
assert set(row["data"].keys()) == set(expected_keys)
verify_values(rows, traces)
@pytest.mark.parametrize(
"order_by,aggregation_alias,expected_status",
[

View File

@@ -36,12 +36,22 @@ from fixtures.querier import (
assert_grouped_scalar,
assert_raw_row_subset,
assert_scalar_value,
find_named_result,
format_timestamp,
generate_traces_with_corrupt_metadata,
get_rows,
make_query_request,
)
from fixtures.traces import TraceIdGenerator, Traces, TracesKind, TracesStatusCode
from fixtures.traces import (
ALL_SELECT_FIELDS,
TraceIdGenerator,
Traces,
TracesEvent,
TracesKind,
TracesLink,
TracesRefType,
TracesStatusCode,
)
def _names(response: requests.Response) -> set:
@@ -613,3 +623,178 @@ def test_trace_operator_with_adjusted_keys(
assert response.status_code == HTTPStatus.OK, response.text
assert_result(response, traces)
# Hardcoded core columns the trace_operator buildListQuery always projects,
# in addition to any user-supplied selectFields.
TRACE_OPERATOR_CORE_FIELDS = [
"timestamp",
"trace_id",
"span_id",
"name",
"duration_nano",
"parent_span_id",
]
def _verify_full_expansion(rows: list[dict], parent_trace: Traces) -> None:
"""Empty-selectFields case: every column from the builder_query parity set
arrives, and events/links are parsed into structured form (refType is
dropped at the consume layer).
"""
assert len(rows) == 1
parent_row = rows[0]["data"]
assert set(parent_row.keys()) == set(ALL_SELECT_FIELDS)
assert parent_row["events"] == parent_trace.events
assert parent_row["links"] == parent_trace.links
for link in parent_row["links"]:
assert "refType" not in link
def _verify_explicit_projection(rows: list[dict], parent_trace: Traces) -> None: # pylint: disable=unused-argument
"""Explicit-selectFields case: only the 6 hardcoded core fields plus the
user-supplied resource.service.name come back. Contextual columns
(events/links/attributes/resource) and the rest of the intrinsics never
appear because the consume-layer merge isn't triggered.
"""
assert len(rows) == 1
parent_row = rows[0]["data"]
assert set(parent_row.keys()) == set(TRACE_OPERATOR_CORE_FIELDS + ["service.name"])
@pytest.mark.parametrize(
"select_fields,verify_values",
[
pytest.param([], _verify_full_expansion, id="empty-select-fields"),
pytest.param(
[{"name": "service.name", "fieldContext": "resource"}],
_verify_explicit_projection,
id="explicit-service-name",
),
],
)
def test_trace_operator_select_fields(
signoz: types.SigNoz,
create_user_admin: None, # pylint: disable=unused-argument
get_token: Callable[[str, str], str],
insert_traces: Callable[[list[Traces]], None],
select_fields: list[dict[str, Any]],
verify_values: Callable[[list[dict], Traces], None],
) -> None:
"""
Setup:
Insert a parent (operation.type = 'parent') with one event and one
user-supplied link, plus a child span (operation.type = 'child').
Tests:
1. With selectFields=[], the `A => B` trace_operator returns every column
in ALL_SELECT_FIELDS, mirroring the builder_query path. Events arrive
as {name, timeUnixNano, attributes} and links as {traceId, spanId}
with refType dropped at the consume layer.
2. With an explicit selectFields=[{"name": "service.name"}], only the 6
hardcoded core columns plus service.name come back — no auto-expansion
to the full set.
See:
- pkg/telemetrytraces/trace_operator_cte_builder.go::buildFinalQuery for
the expansion gate.
- pkg/telemetrytraces/trace_operator_cte_builder.go::buildListQuery for
the per-row SELECT.
"""
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
trace_id = TraceIdGenerator.trace_id()
parent_span_id = TraceIdGenerator.span_id()
child_span_id = TraceIdGenerator.span_id()
parent_event = TracesEvent(
name="request_received",
timestamp=now - timedelta(seconds=4, microseconds=500_000),
attribute_map={"http.method": "GET"},
)
linked_trace_id = TraceIdGenerator.trace_id()
linked_span_id = TraceIdGenerator.span_id()
user_link = TracesLink(
trace_id=linked_trace_id,
span_id=linked_span_id,
ref_type=TracesRefType.REF_TYPE_FOLLOWS_FROM,
)
parent_trace = Traces(
timestamp=now - timedelta(seconds=5),
duration=timedelta(seconds=4),
trace_id=trace_id,
span_id=parent_span_id,
parent_span_id="",
name="parent-operation",
kind=TracesKind.SPAN_KIND_SERVER,
status_code=TracesStatusCode.STATUS_CODE_OK,
resources={"service.name": "trace-operator-query"},
attributes={"operation.type": "parent"},
events=[parent_event],
links=[user_link],
)
child_trace = Traces(
timestamp=now - timedelta(seconds=4),
duration=timedelta(seconds=1),
trace_id=trace_id,
span_id=child_span_id,
parent_span_id=parent_span_id,
name="child-operation",
kind=TracesKind.SPAN_KIND_INTERNAL,
status_code=TracesStatusCode.STATUS_CODE_OK,
resources={"service.name": "trace-operator-query"},
attributes={"operation.type": "child"},
)
insert_traces([parent_trace, child_trace])
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
operator_spec: dict[str, Any] = {
"name": "C",
"expression": "A => B",
"limit": 10,
"order": [{"key": {"name": "timestamp"}, "direction": "asc"}],
}
if select_fields:
operator_spec["selectFields"] = select_fields
queries = [
{
"type": "builder_query",
"spec": {
"name": "A",
"signal": "traces",
"filter": {"expression": "operation.type = 'parent'"},
"limit": 100,
"disabled": True,
},
},
{
"type": "builder_query",
"spec": {
"name": "B",
"signal": "traces",
"filter": {"expression": "operation.type = 'child'"},
"limit": 100,
"disabled": True,
},
},
{"type": "builder_trace_operator", "spec": operator_spec},
]
response = make_query_request(
signoz,
token,
start_ms=int((datetime.now(tz=UTC) - timedelta(minutes=5)).timestamp() * 1000),
end_ms=int(datetime.now(tz=UTC).timestamp() * 1000),
request_type="raw",
queries=queries,
)
assert response.status_code == HTTPStatus.OK
results = response.json()["data"]["data"]["results"]
trace_operator_result = find_named_result(results, "C")
rows = trace_operator_result["rows"]
verify_values(rows, parent_trace)