Compare commits

..

1 Commits

Author SHA1 Message Date
Abhi Kumar
f00d52f8bb chore: added changes to auto-run query when disabled, changed groupby and changed orderBy 2026-05-19 15:55:49 +05:30
15 changed files with 302 additions and 206 deletions

3
.gitignore vendored
View File

@@ -231,5 +231,4 @@ cython_debug/
# LSP config files
pyrightconfig.json
# agents
*settings.local.json

View File

@@ -66,7 +66,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
handleChangeQueryData('groupBy', value, { runAfterUpdate: true });
},
[handleChangeQueryData],
);

View File

@@ -283,14 +283,14 @@ function QueryAddOns({
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
handleChangeQueryData('groupBy', value, { runAfterUpdate: true });
},
[handleChangeQueryData],
);
const handleChangeOrderByKeys = useCallback(
(value: IBuilderQuery['orderBy']) => {
handleChangeQueryData('orderBy', value);
handleChangeQueryData('orderBy', value, { runAfterUpdate: true });
},
[handleChangeQueryData],
);

View File

@@ -73,8 +73,8 @@ export const QueryV2 = forwardRef(function QueryV2(
});
const handleToggleDisableQuery = useCallback(() => {
handleChangeQueryData('disabled', !query.disabled);
}, [handleChangeQueryData, query]);
handleChangeQueryData('disabled', !query.disabled, { runAfterUpdate: true });
}, [handleChangeQueryData, query.disabled]);
const handleToggleCollapsQuery = (): void => {
setIsCollapsed(!isCollapsed);

View File

@@ -5,7 +5,8 @@ import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import {
DataSource,
MetricAggregateOperator,
@@ -333,3 +334,196 @@ describe('useQueryBuilderOperations - Empty Aggregate Attribute Type', () => {
});
});
});
describe('useQueryBuilderOperations - handleChangeQueryData runAfterUpdate', () => {
const mockHandleSetQueryData = jest.fn();
const mockHandleSetTraceOperatorData = jest.fn();
const mockHandleRunQuery = jest.fn();
const baseQuery: IBuilderQuery = {
dataSource: DataSource.METRICS,
aggregateOperator: MetricAggregateOperator.AVG,
aggregateAttribute: {
key: 'system.cpu.load',
dataType: DataTypes.Float64,
type: ATTRIBUTE_TYPES.GAUGE,
} as BaseAutocompleteData,
timeAggregation: MetricAggregateOperator.AVG,
spaceAggregation: '',
aggregations: [],
having: [],
limit: null,
queryName: 'A',
functions: [],
filters: { items: [], op: 'AND' },
groupBy: [],
orderBy: [],
stepInterval: 60,
expression: '',
disabled: false,
reduceTo: ReduceOperators.AVG,
legend: '',
};
const otherQuery: IBuilderQuery = { ...baseQuery, queryName: 'B' };
const buildCurrentQuery = (): Query => ({
queryType: EQueryType.QUERY_BUILDER,
promql: [],
clickhouse_sql: [],
id: 'q-1',
unit: '',
builder: {
queryData: [baseQuery, otherQuery],
queryFormulas: [],
queryTraceOperator: [],
},
});
const setupMock = (overrides: Record<string, unknown> = {}): void => {
(useQueryBuilder as jest.Mock).mockReturnValue({
handleSetQueryData: mockHandleSetQueryData,
handleSetTraceOperatorData: mockHandleSetTraceOperatorData,
handleSetFormulaData: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
setLastUsedQuery: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
handleRunQuery: mockHandleRunQuery,
panelType: 'time_series',
currentQuery: buildCurrentQuery(),
...overrides,
});
};
beforeEach(() => {
jest.clearAllMocks();
setupMock();
});
it('does not call handleRunQuery when options is omitted', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: baseQuery,
index: 0,
entityVersion: ENTITY_VERSION_V4,
}),
);
act(() => {
result.current.handleChangeQueryData('legend', 'cpu-load');
});
expect(mockHandleSetQueryData).toHaveBeenCalledWith(
0,
expect.objectContaining({ legend: 'cpu-load' }),
);
expect(mockHandleRunQuery).not.toHaveBeenCalled();
});
it('calls handleRunQuery with the freshly-changed query when runAfterUpdate is true', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: baseQuery,
index: 0,
entityVersion: ENTITY_VERSION_V4,
}),
);
act(() => {
result.current.handleChangeQueryData('disabled', true, {
runAfterUpdate: true,
});
});
expect(mockHandleSetQueryData).toHaveBeenCalledWith(
0,
expect.objectContaining({ disabled: true }),
);
expect(mockHandleRunQuery).toHaveBeenCalledTimes(1);
const [override] = mockHandleRunQuery.mock.calls[0];
// Index 0 reflects the new value...
expect(override.builder.queryData[0]).toStrictEqual(
expect.objectContaining({ queryName: 'A', disabled: true }),
);
// ...siblings stay untouched.
expect(override.builder.queryData[1]).toStrictEqual(
expect.objectContaining({ queryName: 'B', disabled: false }),
);
});
it('applies the change at the correct index without disturbing other queries', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: otherQuery,
index: 1,
entityVersion: ENTITY_VERSION_V4,
}),
);
act(() => {
result.current.handleChangeQueryData(
'groupBy',
[
{
key: 'host.name',
type: 'tag',
dataType: DataTypes.String,
} as BaseAutocompleteData,
],
{ runAfterUpdate: true },
);
});
const [override] = mockHandleRunQuery.mock.calls[0];
expect(override.builder.queryData[0]).toStrictEqual(
expect.objectContaining({ queryName: 'A', groupBy: [] }),
);
expect(override.builder.queryData[1]).toStrictEqual(
expect.objectContaining({
queryName: 'B',
groupBy: [expect.objectContaining({ key: 'host.name' })],
}),
);
});
it('keeps handleSetQueryData and handleRunQuery in sync for legend formatting', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: baseQuery,
index: 0,
entityVersion: ENTITY_VERSION_V4,
}),
);
act(() => {
result.current.handleChangeQueryData('legend', '{{service.name}}', {
runAfterUpdate: true,
});
});
const [override] = mockHandleRunQuery.mock.calls[0];
const setCallLegend = mockHandleSetQueryData.mock.calls[0][1].legend;
expect(override.builder.queryData[0].legend).toBe(setCallLegend);
});
it('does not call handleRunQuery for trace-operator queries (early return)', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: baseQuery,
index: 0,
entityVersion: ENTITY_VERSION_V4,
isForTraceOperator: true,
}),
);
act(() => {
result.current.handleChangeQueryData('disabled', true, {
runAfterUpdate: true,
});
});
expect(mockHandleSetTraceOperatorData).toHaveBeenCalledTimes(1);
expect(mockHandleSetQueryData).not.toHaveBeenCalled();
expect(mockHandleRunQuery).not.toHaveBeenCalled();
});
});

View File

@@ -45,6 +45,7 @@ import {
import {
HandleChangeFormulaData,
HandleChangeQueryData,
HandleChangeQueryDataOptions,
HandleChangeQueryDataV5,
UseQueryOperations,
} from 'types/common/operations.types';
@@ -76,6 +77,7 @@ export const useQueryOperations: UseQueryOperations = ({
currentQuery,
setLastUsedQuery,
redirectWithQueryBuilderData,
handleRunQuery,
} = useQueryBuilder();
const [operators, setOperators] = useState<SelectOption<string, string>[]>([]);
@@ -530,7 +532,7 @@ export const useQueryOperations: UseQueryOperations = ({
const handleChangeQueryData: HandleChangeQueryData | HandleChangeQueryDataV5 =
useCallback(
(key: string, value: any) => {
(key: string, value: any, options?: HandleChangeQueryDataOptions) => {
const newQuery = {
...query,
[key]:
@@ -541,8 +543,24 @@ export const useQueryOperations: UseQueryOperations = ({
if (isForTraceOperator) {
handleSetTraceOperatorData(index, newQuery);
} else {
handleSetQueryData(index, newQuery);
return;
}
handleSetQueryData(index, newQuery);
// `runAfterUpdate` lets callers stage-and-run inline. We pass the
// freshly-computed query straight to `handleRunQuery` because the
// setState above hasn't flushed yet.
if (options?.runAfterUpdate) {
handleRunQuery({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, i) =>
i === index ? newQuery : item,
),
},
});
}
},
[
@@ -551,6 +569,8 @@ export const useQueryOperations: UseQueryOperations = ({
handleSetQueryData,
handleSetTraceOperatorData,
isForTraceOperator,
handleRunQuery,
currentQuery,
],
);

View File

@@ -1024,49 +1024,57 @@ export function QueryBuilderProvider({
[],
);
const handleRunQuery = useCallback(() => {
const isExplorer =
location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER;
if (isExplorer) {
setCalledFromHandleRunQuery(true);
}
const currentQueryData = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
filter: {
...item.filter,
expression:
item.filter?.expression.trim() === ''
? ''
: (item.filter?.expression ?? ''),
},
filters: {
items: [],
op: 'AND',
},
})),
},
};
// `overrideQuery` lets callers run a query value that hasn't been committed
// to `currentQuery` state yet — e.g. a click handler that toggles a flag
// and wants to stage-and-run in the same tick, without waiting for the
// state update to flush.
const handleRunQuery = useCallback(
(overrideQuery?: Query) => {
const isExplorer =
location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER;
if (isExplorer) {
setCalledFromHandleRunQuery(true);
}
const sourceQuery = overrideQuery ?? currentQuery;
const currentQueryData = {
...sourceQuery,
builder: {
...sourceQuery.builder,
queryData: sourceQuery.builder.queryData.map((item) => ({
...item,
filter: {
...item.filter,
expression:
item.filter?.expression.trim() === ''
? ''
: (item.filter?.expression ?? ''),
},
filters: {
items: [],
op: 'AND',
},
})),
},
};
redirectWithQueryBuilderData({
...{
...currentQueryData,
...updateStepInterval({
builder: currentQueryData.builder,
clickhouse_sql: currentQueryData.clickhouse_sql,
promql: currentQueryData.promql,
id: currentQueryData.id,
queryType,
unit: currentQueryData.unit,
}),
},
queryType,
});
}, [currentQuery, location.pathname, queryType, redirectWithQueryBuilderData]);
redirectWithQueryBuilderData({
...{
...currentQueryData,
...updateStepInterval({
builder: currentQueryData.builder,
clickhouse_sql: currentQueryData.clickhouse_sql,
promql: currentQueryData.promql,
id: currentQueryData.id,
queryType,
unit: currentQueryData.unit,
}),
},
queryType,
});
},
[currentQuery, location.pathname, queryType, redirectWithQueryBuilderData],
);
useEffect(() => {
if (location.pathname !== currentPathnameRef.current) {

View File

@@ -26,6 +26,16 @@ type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
savePreviousQuery?: boolean;
};
export interface HandleChangeQueryDataOptions {
/**
* When true, stage-and-run the query immediately after the local state
* update — no need to wait for the user to click "Stage and Run".
* Useful for inline toggles (visibility, disable, etc.) where the panel
* should reflect the change without an extra click.
*/
runAfterUpdate?: boolean;
}
// Generic type that can work with both legacy and V5 query types
export type HandleChangeQueryData<T = IBuilderQuery> = <
Key extends keyof T,
@@ -33,6 +43,7 @@ export type HandleChangeQueryData<T = IBuilderQuery> = <
>(
key: Key,
value: Value,
options?: HandleChangeQueryDataOptions,
) => void;
export type HandleChangeTraceOperatorData<T = IBuilderTraceOperator> = <

View File

@@ -280,7 +280,7 @@ export type QueryBuilderContextType = {
shallStringify?: boolean,
newTab?: boolean,
) => void;
handleRunQuery: () => void;
handleRunQuery: (overrideQuery?: Query) => void;
resetQuery: (newCurrentQuery?: QueryState) => void;
handleOnUnitsChange: (units: Format['id']) => void;
updateAllQueriesOperators: (

View File

@@ -56,15 +56,6 @@ func QueryStringToKeysSelectors(query string) []*telemetrytypes.FieldKeySelector
FieldDataType: key.FieldDataType,
})
}
if key.FieldContext == telemetrytypes.FieldContextScope {
keys = append(keys, &telemetrytypes.FieldKeySelector{
Name: key.FieldContext.StringValue() + "." + key.Name,
Signal: key.Signal,
FieldContext: key.FieldContext,
FieldDataType: key.FieldDataType,
})
}
}
}

View File

@@ -200,7 +200,7 @@ func (t *telemetryMetaStore) getTracesKeys(ctx context.Context, fieldKeySelector
`CASE
// WHEN tagType = 'spanfield' THEN 1
WHEN tagType = 'resource' THEN 2
WHEN tagType = 'scope' THEN 3
// WHEN tagType = 'scope' THEN 3
WHEN tagType = 'tag' THEN 4
ELSE 5
END as priority`,

View File

@@ -51,7 +51,6 @@ var (
ValueType: schema.ColumnTypeString,
}},
"resource": {Name: "resource", Type: schema.JSONColumnType{}},
"scope": {Name: "scope", Type: schema.JSONColumnType{}},
"events": {Name: "events", Type: schema.ArrayColumnType{
ElementType: schema.ColumnTypeString,
@@ -177,7 +176,7 @@ func (m *defaultFieldMapper) getColumn(
case telemetrytypes.FieldContextResource:
return []*schema.Column{indexV3Columns["resource"]}, nil
case telemetrytypes.FieldContextScope:
return []*schema.Column{indexV3Columns["scope"]}, nil
return []*schema.Column{}, qbtypes.ErrColumnNotFound
case telemetrytypes.FieldContextAttribute:
switch key.FieldDataType {
case telemetrytypes.FieldDataTypeString:
@@ -262,28 +261,21 @@ func (m *defaultFieldMapper) FieldFor(
switch column.Type.GetType() {
case schema.ColumnTypeEnumJSON:
switch key.FieldContext {
case telemetrytypes.FieldContextScope:
switch key.Name {
case "scope.name", "scope.version":
return fmt.Sprintf("%s::String", key.Name), nil
default:
return fmt.Sprintf("%s.attributes.`%s`::String", column.Name, key.Name), nil
}
case telemetrytypes.FieldContextResource:
oldColumn := indexV3Columns["resources_string"]
oldKeyName := fmt.Sprintf("%s['%s']", oldColumn.Name, key.Name)
// have to add ::string as clickHouse throws an error :- data types Variant/Dynamic are not allowed in GROUP BY
// once clickHouse dependency is updated, we need to check if we can remove it.
if key.Materialized {
oldKeyName = telemetrytypes.FieldKeyToMaterializedColumnName(key)
oldKeyNameExists := telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, %s==true, %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldKeyNameExists, oldKeyName), nil
} else {
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
}
// json is only supported for resource context as of now
if key.FieldContext != telemetrytypes.FieldContextResource {
return "", errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "only resource context fields are supported for json columns, got %s", key.FieldContext.String)
}
oldColumn := indexV3Columns["resources_string"]
oldKeyName := fmt.Sprintf("%s['%s']", oldColumn.Name, key.Name)
// have to add ::string as clickHouse throws an error :- data types Variant/Dynamic are not allowed in GROUP BY
// once clickHouse dependency is updated, we need to check if we can remove it.
if key.Materialized {
oldKeyName = telemetrytypes.FieldKeyToMaterializedColumnName(key)
oldKeyNameExists := telemetrytypes.FieldKeyToMaterializedColumnNameForExists(key)
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, %s==true, %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldKeyNameExists, oldKeyName), nil
} else {
return fmt.Sprintf("multiIf(%s.`%s` IS NOT NULL, %s.`%s`::String, mapContains(%s, '%s'), %s, NULL)", column.Name, key.Name, column.Name, key.Name, oldColumn.Name, key.Name, oldKeyName), nil
}
return "", errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "json column type only supported for resource and scope context, got %s", key.FieldContext.String)
case schema.ColumnTypeEnumString,
schema.ColumnTypeEnumUInt64,
schema.ColumnTypeEnumUInt32,

View File

@@ -78,51 +78,6 @@ 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,
},
{
name: "Scope field - name",
key: telemetrytypes.TelemetryFieldKey{
Name: "name",
FieldContext: telemetrytypes.FieldContextScope,
},
expectedResult: "scope.name::String",
expectedError: nil,
},
{
name: "Scope field - scope.name (denormalized)",
key: telemetrytypes.TelemetryFieldKey{
Name: "scope.name",
FieldContext: telemetrytypes.FieldContextScope,
},
expectedResult: "scope.name::String",
expectedError: nil,
},
{
name: "Scope field - version",
key: telemetrytypes.TelemetryFieldKey{
Name: "version",
FieldContext: telemetrytypes.FieldContextScope,
},
expectedResult: "scope.version::String",
expectedError: nil,
},
{
name: "Scope field - scope.version (denormalized)",
key: telemetrytypes.TelemetryFieldKey{
Name: "scope.version",
FieldContext: telemetrytypes.FieldContextScope,
},
expectedResult: "scope.version::String",
expectedError: nil,
},
{
name: "Scope field - custom attribute",
key: telemetrytypes.TelemetryFieldKey{
Name: "custom.attr",
FieldContext: telemetrytypes.FieldContextScope,
},
expectedResult: "scope.attributes.`custom.attr`::String",
expectedError: nil,
},
{
name: "Non-existent column",
key: telemetrytypes.TelemetryFieldKey{

View File

@@ -350,66 +350,6 @@ func TestStatementBuilder(t *testing.T) {
},
expectedErr: nil,
},
{
name: "scope.name filter and group by",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Aggregations: []qbtypes.TraceAggregation{
{
Expression: "count()",
},
},
Filter: &qbtypes.Filter{
Expression: "scope.name = 'opentelemetry-io'",
},
Limit: 10,
GroupBy: []qbtypes.GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "scope.name",
FieldContext: telemetrytypes.FieldContextScope,
},
},
},
},
expected: qbtypes.Statement{
Query: "WITH __limit_cte AS (SELECT toString(multiIf(scope.name::String IS NOT NULL, scope.name::String, NULL)) AS `scope.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE (scope.name::String = ? AND scope.name::String IS NOT NULL) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `scope.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(scope.name::String IS NOT NULL, scope.name::String, NULL)) AS `scope.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE (scope.name::String = ? AND scope.name::String IS NOT NULL) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`scope.name`) GLOBAL IN (SELECT `scope.name` FROM __limit_cte) GROUP BY ts, `scope.name`",
Args: []any{"opentelemetry-io", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "opentelemetry-io", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
{
name: "scope.version filter with scope.name group by",
requestType: qbtypes.RequestTypeTimeSeries,
query: qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]{
Signal: telemetrytypes.SignalTraces,
StepInterval: qbtypes.Step{Duration: 30 * time.Second},
Aggregations: []qbtypes.TraceAggregation{
{
Expression: "count()",
},
},
Filter: &qbtypes.Filter{
Expression: "scope.version = '1.0.0'",
},
Limit: 10,
GroupBy: []qbtypes.GroupByKey{
{
TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{
Name: "scope.name",
FieldContext: telemetrytypes.FieldContextScope,
},
},
},
},
expected: qbtypes.Statement{
Query: "WITH __limit_cte AS (SELECT toString(multiIf(scope.name::String IS NOT NULL, scope.name::String, NULL)) AS `scope.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE (scope.version::String = ? AND scope.version::String IS NOT NULL) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? GROUP BY `scope.name` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(timestamp, INTERVAL 30 SECOND) AS ts, toString(multiIf(scope.name::String IS NOT NULL, scope.name::String, NULL)) AS `scope.name`, count() AS __result_0 FROM signoz_traces.distributed_signoz_index_v3 WHERE (scope.version::String = ? AND scope.version::String IS NOT NULL) AND timestamp >= ? AND timestamp < ? AND ts_bucket_start >= ? AND ts_bucket_start <= ? AND (`scope.name`) GLOBAL IN (SELECT `scope.name` FROM __limit_cte) GROUP BY ts, `scope.name`",
Args: []any{"1.0.0", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448), 10, "1.0.0", "1747947419000000000", "1747983448000000000", uint64(1747945619), uint64(1747983448)},
},
expectedErr: nil,
},
}
fm := NewFieldMapper()

View File

@@ -111,20 +111,6 @@ func buildCompleteFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
FieldDataType: telemetrytypes.FieldDataTypeBool,
},
},
"scope.name": {
{
Name: "scope.name",
FieldContext: telemetrytypes.FieldContextScope,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
"scope.version": {
{
Name: "scope.version",
FieldContext: telemetrytypes.FieldContextScope,
FieldDataType: telemetrytypes.FieldDataTypeString,
},
},
}
for _, keys := range keysMap {
for _, key := range keys {