@oneuptime/common 11.2.3 → 11.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/Models/AnalyticsModels/Span.ts +285 -0
  2. package/Server/Infrastructure/Queue.ts +20 -0
  3. package/Server/Services/ProjectService.ts +429 -300
  4. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +26 -4
  5. package/Server/Utils/Monitor/MonitorResource.ts +19 -1
  6. package/Server/Utils/Telemetry/LlmSpan.ts +251 -0
  7. package/Server/Utils/Telemetry.ts +154 -7
  8. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +25 -12
  9. package/Tests/Server/Utils/Telemetry/LlmSpan.test.ts +164 -0
  10. package/Tests/Server/Utils/Telemetry.test.ts +251 -0
  11. package/Types/Dashboard/DashboardComponents/DashboardTraceChartComponent.ts +8 -3
  12. package/UI/Components/Navbar/NavBar.tsx +13 -2
  13. package/Utils/Dashboard/Components/DashboardTraceChartComponent.ts +6 -58
  14. package/build/dist/Models/AnalyticsModels/Span.js +231 -0
  15. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  16. package/build/dist/Server/Infrastructure/Queue.js +7 -1
  17. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  18. package/build/dist/Server/Services/ProjectService.js +371 -277
  19. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  20. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +26 -4
  21. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  22. package/build/dist/Server/Utils/Monitor/MonitorResource.js +17 -1
  23. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  24. package/build/dist/Server/Utils/Telemetry/LlmSpan.js +153 -0
  25. package/build/dist/Server/Utils/Telemetry/LlmSpan.js.map +1 -0
  26. package/build/dist/Server/Utils/Telemetry.js +128 -7
  27. package/build/dist/Server/Utils/Telemetry.js.map +1 -1
  28. package/build/dist/UI/Components/Navbar/NavBar.js +4 -1
  29. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  30. package/build/dist/Utils/Dashboard/Components/DashboardTraceChartComponent.js +6 -49
  31. package/build/dist/Utils/Dashboard/Components/DashboardTraceChartComponent.js.map +1 -1
  32. package/package.json +1 -1
@@ -0,0 +1,164 @@
1
+ import LlmSpanUtil, {
2
+ LlmSpanFields,
3
+ } from "../../../../Server/Utils/Telemetry/LlmSpan";
4
+ import { AttributeType } from "../../../../Server/Utils/Telemetry/Telemetry";
5
+ import Dictionary from "../../../../Types/Dictionary";
6
+ import { describe, expect, test } from "@jest/globals";
7
+
8
+ type Attrs = Dictionary<AttributeType | Array<AttributeType>>;
9
+
10
+ describe("LlmSpanUtil.extract", () => {
11
+ test("non-LLM span returns empty/default fields", () => {
12
+ const attrs: Attrs = {
13
+ "http.method": "GET",
14
+ "http.route": "/api/users",
15
+ "db.system": "postgresql",
16
+ };
17
+
18
+ const fields: LlmSpanFields = LlmSpanUtil.extract(attrs);
19
+
20
+ expect(fields.isLlmSpan).toBe(false);
21
+ expect(fields.llmSystem).toBe("");
22
+ expect(fields.llmTotalTokens).toBe(0);
23
+ expect(fields.llmCost).toBe(0);
24
+ });
25
+
26
+ test("empty attributes returns defaults", () => {
27
+ const fields: LlmSpanFields = LlmSpanUtil.extract({});
28
+ expect(fields).toEqual(LlmSpanUtil.empty());
29
+ });
30
+
31
+ test("OTel GenAI conventions (chat completion)", () => {
32
+ const attrs: Attrs = {
33
+ "gen_ai.system": "openai",
34
+ "gen_ai.operation.name": "chat",
35
+ "gen_ai.request.model": "gpt-4o",
36
+ "gen_ai.response.model": "gpt-4o-2024-08-06",
37
+ "gen_ai.usage.input_tokens": 1200,
38
+ "gen_ai.usage.output_tokens": 350,
39
+ "gen_ai.usage.cost": 0.0185,
40
+ };
41
+
42
+ const fields: LlmSpanFields = LlmSpanUtil.extract(attrs);
43
+
44
+ expect(fields.isLlmSpan).toBe(true);
45
+ expect(fields.llmSystem).toBe("openai");
46
+ expect(fields.llmOperation).toBe("chat");
47
+ expect(fields.llmRequestModel).toBe("gpt-4o");
48
+ expect(fields.llmResponseModel).toBe("gpt-4o-2024-08-06");
49
+ expect(fields.llmInputTokens).toBe(1200);
50
+ expect(fields.llmOutputTokens).toBe(350);
51
+ // total derived from input + output when not reported.
52
+ expect(fields.llmTotalTokens).toBe(1550);
53
+ expect(fields.llmCost).toBe(0.0185);
54
+ });
55
+
56
+ test("explicit total_tokens is preferred over derived sum", () => {
57
+ const attrs: Attrs = {
58
+ "gen_ai.system": "anthropic",
59
+ "gen_ai.request.model": "claude-opus-4-8",
60
+ "gen_ai.usage.input_tokens": 100,
61
+ "gen_ai.usage.output_tokens": 50,
62
+ "gen_ai.usage.total_tokens": 999,
63
+ };
64
+
65
+ const fields: LlmSpanFields = LlmSpanUtil.extract(attrs);
66
+ expect(fields.llmTotalTokens).toBe(999);
67
+ });
68
+
69
+ test("OpenLLMetry legacy prompt/completion token aliases", () => {
70
+ const attrs: Attrs = {
71
+ "gen_ai.system": "anthropic",
72
+ "gen_ai.request.model": "claude-sonnet-4-6",
73
+ "gen_ai.usage.prompt_tokens": 80,
74
+ "gen_ai.usage.completion_tokens": 20,
75
+ };
76
+
77
+ const fields: LlmSpanFields = LlmSpanUtil.extract(attrs);
78
+ expect(fields.isLlmSpan).toBe(true);
79
+ expect(fields.llmInputTokens).toBe(80);
80
+ expect(fields.llmOutputTokens).toBe(20);
81
+ expect(fields.llmTotalTokens).toBe(100);
82
+ });
83
+
84
+ test("OpenInference (llm.* + llm.token_count.*) conventions", () => {
85
+ const attrs: Attrs = {
86
+ "openinference.span.kind": "LLM",
87
+ "llm.system": "openai",
88
+ "llm.model_name": "gpt-4o-mini",
89
+ "llm.token_count.prompt": 500,
90
+ "llm.token_count.completion": 120,
91
+ "llm.token_count.total": 620,
92
+ };
93
+
94
+ const fields: LlmSpanFields = LlmSpanUtil.extract(attrs);
95
+ expect(fields.isLlmSpan).toBe(true);
96
+ expect(fields.llmSystem).toBe("openai");
97
+ expect(fields.llmOperation).toBe("LLM");
98
+ expect(fields.llmRequestModel).toBe("gpt-4o-mini");
99
+ expect(fields.llmInputTokens).toBe(500);
100
+ expect(fields.llmOutputTokens).toBe(120);
101
+ expect(fields.llmTotalTokens).toBe(620);
102
+ });
103
+
104
+ test("numeric token values reported as strings are coerced", () => {
105
+ const attrs: Attrs = {
106
+ "gen_ai.system": "openai",
107
+ "gen_ai.usage.input_tokens": "42",
108
+ "gen_ai.usage.output_tokens": "8",
109
+ };
110
+
111
+ const fields: LlmSpanFields = LlmSpanUtil.extract(attrs);
112
+ expect(fields.llmInputTokens).toBe(42);
113
+ expect(fields.llmOutputTokens).toBe(8);
114
+ expect(fields.llmTotalTokens).toBe(50);
115
+ });
116
+
117
+ test("agent + tool spans are detected", () => {
118
+ const agentAttrs: Attrs = {
119
+ "gen_ai.operation.name": "invoke_agent",
120
+ "gen_ai.agent.name": "research-agent",
121
+ };
122
+ const agentFields: LlmSpanFields = LlmSpanUtil.extract(agentAttrs);
123
+ expect(agentFields.isLlmSpan).toBe(true);
124
+ expect(agentFields.llmAgentName).toBe("research-agent");
125
+ expect(agentFields.llmOperation).toBe("invoke_agent");
126
+
127
+ const toolAttrs: Attrs = {
128
+ "gen_ai.operation.name": "execute_tool",
129
+ "gen_ai.tool.name": "web_search",
130
+ };
131
+ const toolFields: LlmSpanFields = LlmSpanUtil.extract(toolAttrs);
132
+ expect(toolFields.isLlmSpan).toBe(true);
133
+ expect(toolFields.llmToolName).toBe("web_search");
134
+ });
135
+
136
+ test("response model is used as request model fallback", () => {
137
+ const attrs: Attrs = {
138
+ "gen_ai.system": "openai",
139
+ "gen_ai.response.model": "gpt-4o-2024-08-06",
140
+ };
141
+ const fields: LlmSpanFields = LlmSpanUtil.extract(attrs);
142
+ expect(fields.llmRequestModel).toBe("gpt-4o-2024-08-06");
143
+ expect(fields.llmResponseModel).toBe("gpt-4o-2024-08-06");
144
+ });
145
+
146
+ test("bare gen_ai-namespaced attribute still flags as LLM span", () => {
147
+ const attrs: Attrs = {
148
+ "gen_ai.request.temperature": 0.7,
149
+ };
150
+ const fields: LlmSpanFields = LlmSpanUtil.extract(attrs);
151
+ expect(fields.isLlmSpan).toBe(true);
152
+ });
153
+
154
+ test("array-valued attributes do not break string/number extraction", () => {
155
+ const attrs: Attrs = {
156
+ "gen_ai.system": "openai",
157
+ "gen_ai.response.finish_reasons": ["stop"],
158
+ "gen_ai.usage.input_tokens": 10,
159
+ };
160
+ const fields: LlmSpanFields = LlmSpanUtil.extract(attrs);
161
+ expect(fields.isLlmSpan).toBe(true);
162
+ expect(fields.llmInputTokens).toBe(10);
163
+ });
164
+ });
@@ -0,0 +1,251 @@
1
+ import Telemetry, {
2
+ Span,
3
+ SpanStatusCode,
4
+ } from "../../../Server/Utils/Telemetry";
5
+ import {
6
+ afterAll,
7
+ beforeAll,
8
+ describe,
9
+ expect,
10
+ jest,
11
+ test,
12
+ } from "@jest/globals";
13
+
14
+ type ExceptionAttributes = Record<string, string | undefined>;
15
+
16
+ // getExceptionAttributes is a private static; reach it through a narrow cast.
17
+ function getAttributes(exception: unknown): ExceptionAttributes {
18
+ return (
19
+ Telemetry as unknown as {
20
+ getExceptionAttributes: (e: unknown) => ExceptionAttributes;
21
+ }
22
+ ).getExceptionAttributes(exception);
23
+ }
24
+
25
+ describe("Telemetry.getExceptionAttributes", () => {
26
+ test("extracts Postgres fields from a TypeORM QueryFailedError-shaped error", () => {
27
+ const error: Error = Object.assign(
28
+ new Error('delete on "Project" violates foreign key constraint'),
29
+ {
30
+ driverError: {
31
+ code: "23503",
32
+ detail: 'Key (id)=(abc) is still referenced from table "Monitor".',
33
+ constraint: "FK_monitor_project",
34
+ table: "Monitor",
35
+ column: "projectId",
36
+ schema: "public",
37
+ },
38
+ query: 'DELETE FROM "Project" WHERE "_id" IN ($1)',
39
+ },
40
+ );
41
+
42
+ const attributes: ExceptionAttributes = getAttributes(error);
43
+
44
+ expect(attributes["exception.code"]).toBe("23503");
45
+ expect(attributes["db.error.constraint"]).toBe("FK_monitor_project");
46
+ expect(attributes["db.error.table"]).toBe("Monitor");
47
+ expect(attributes["db.error.column"]).toBe("projectId");
48
+ expect(attributes["db.error.schema"]).toBe("public");
49
+ expect(attributes["db.error.detail"]).toContain("still referenced");
50
+ expect(attributes["db.statement"]).toBe(
51
+ 'DELETE FROM "Project" WHERE "_id" IN ($1)',
52
+ );
53
+ expect(attributes["exception.type"]).toBe("Error");
54
+ expect(attributes["exception.message"]).toContain("violates foreign key");
55
+ });
56
+
57
+ test("reads a top-level pg error code when there is no driverError", () => {
58
+ const error: Error = Object.assign(
59
+ new Error("duplicate key value violates unique constraint"),
60
+ { code: "23505", constraint: "uniq_email" },
61
+ );
62
+
63
+ const attributes: ExceptionAttributes = getAttributes(error);
64
+
65
+ expect(attributes["exception.code"]).toBe("23505");
66
+ expect(attributes["db.error.constraint"]).toBe("uniq_email");
67
+ });
68
+
69
+ test("captures type, message and stacktrace for a plain Error", () => {
70
+ const attributes: ExceptionAttributes = getAttributes(new Error("boom"));
71
+
72
+ expect(attributes["exception.type"]).toBe("Error");
73
+ expect(attributes["exception.message"]).toBe("boom");
74
+ expect(typeof attributes["exception.stacktrace"]).toBe("string");
75
+ });
76
+
77
+ test("handles a thrown string", () => {
78
+ const attributes: ExceptionAttributes = getAttributes("kaboom");
79
+
80
+ expect(attributes["exception.message"]).toBe("kaboom");
81
+ expect(attributes["exception.type"]).toBeUndefined();
82
+ expect(attributes["db.error.constraint"]).toBeUndefined();
83
+ });
84
+
85
+ test("handles null and undefined throws", () => {
86
+ expect(getAttributes(null)["exception.message"]).toBe(
87
+ "Unknown error: null or undefined was thrown",
88
+ );
89
+ expect(getAttributes(undefined)["exception.message"]).toBe(
90
+ "Unknown error: null or undefined was thrown",
91
+ );
92
+ });
93
+
94
+ test("serializes a non-Error object throw into the message", () => {
95
+ const attributes: ExceptionAttributes = getAttributes({ a: 1, b: "x" });
96
+
97
+ expect(attributes["exception.message"]).toBe('{"a":1,"b":"x"}');
98
+ });
99
+
100
+ test("truncates an oversized message and SQL statement", () => {
101
+ const error: Error = Object.assign(new Error("x".repeat(9000)), {
102
+ query: `SELECT ${"a".repeat(9000)}`,
103
+ });
104
+
105
+ const attributes: ExceptionAttributes = getAttributes(error);
106
+
107
+ expect(attributes["exception.message"]?.length).toBe(4000);
108
+ expect(attributes["db.statement"]?.length).toBe(2000);
109
+ });
110
+
111
+ // --- crash-safety: hostile thrown values must never make this throw. ---
112
+
113
+ test("does not throw on an object with throwing getters", () => {
114
+ const hostile: Record<string, unknown> = {};
115
+ Object.defineProperty(hostile, "code", {
116
+ enumerable: true,
117
+ get: (): never => {
118
+ throw new Error("getter blew up");
119
+ },
120
+ });
121
+ Object.defineProperty(hostile, "driverError", {
122
+ enumerable: true,
123
+ get: (): never => {
124
+ throw new Error("driverError blew up");
125
+ },
126
+ });
127
+
128
+ let attributes: ExceptionAttributes = {};
129
+ expect((): void => {
130
+ attributes = getAttributes(hostile);
131
+ }).not.toThrow();
132
+ expect(typeof attributes["exception.message"]).toBe("string");
133
+ });
134
+
135
+ test("does not throw on a field whose toString throws (good fields survive)", () => {
136
+ const error: Error = Object.assign(new Error("db fail"), {
137
+ driverError: {
138
+ detail: {
139
+ toString: (): never => {
140
+ throw new Error("toString blew up");
141
+ },
142
+ },
143
+ },
144
+ });
145
+
146
+ expect((): ExceptionAttributes => {
147
+ return getAttributes(error);
148
+ }).not.toThrow();
149
+ expect(getAttributes(error)["exception.message"]).toBe("db fail");
150
+ });
151
+
152
+ test("does not throw on a Proxy that traps every property read", () => {
153
+ const hostile: unknown = new Proxy(
154
+ {},
155
+ {
156
+ get: (): never => {
157
+ throw new Error("proxy trap");
158
+ },
159
+ },
160
+ );
161
+
162
+ expect((): ExceptionAttributes => {
163
+ return getAttributes(hostile);
164
+ }).not.toThrow();
165
+ });
166
+ });
167
+
168
+ describe("Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan", () => {
169
+ type FakeSpanState = {
170
+ attributes: Record<string, unknown> | null;
171
+ status: { code: number; message?: string } | null;
172
+ recorded: unknown;
173
+ ended: number;
174
+ };
175
+
176
+ // Silence the logger.error console output these tests intentionally trigger.
177
+ beforeAll((): void => {
178
+ jest.spyOn(console, "error").mockImplementation((): void => {
179
+ return undefined;
180
+ });
181
+ });
182
+
183
+ afterAll((): void => {
184
+ jest.restoreAllMocks();
185
+ });
186
+
187
+ function makeFakeSpan(
188
+ state: FakeSpanState,
189
+ opts?: { throwOnSetAttributes?: boolean },
190
+ ): Span {
191
+ return {
192
+ setAttributes: (a: Record<string, unknown>): unknown => {
193
+ if (opts?.throwOnSetAttributes) {
194
+ throw new Error("setAttributes blew up");
195
+ }
196
+ state.attributes = a;
197
+ return undefined;
198
+ },
199
+ recordException: (e: unknown): unknown => {
200
+ state.recorded = e;
201
+ return undefined;
202
+ },
203
+ setStatus: (s: { code: number; message?: string }): unknown => {
204
+ state.status = s;
205
+ return undefined;
206
+ },
207
+ end: (): void => {
208
+ state.ended += 1;
209
+ },
210
+ } as unknown as Span;
211
+ }
212
+
213
+ test("marks the span as error with a message and ends it", () => {
214
+ const state: FakeSpanState = {
215
+ attributes: null,
216
+ status: null,
217
+ recorded: null,
218
+ ended: 0,
219
+ };
220
+
221
+ Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
222
+ span: makeFakeSpan(state),
223
+ exception: new Error("kaboom"),
224
+ });
225
+
226
+ expect(state.ended).toBe(1);
227
+ expect(state.status?.code).toBe(SpanStatusCode.ERROR);
228
+ expect(state.status?.message).toBe("kaboom");
229
+ expect((state.attributes || {})["exception.message"]).toBe("kaboom");
230
+ });
231
+
232
+ test("still ends and flags the span even if a span write throws", () => {
233
+ const state: FakeSpanState = {
234
+ attributes: null,
235
+ status: null,
236
+ recorded: null,
237
+ ended: 0,
238
+ };
239
+ const span: Span = makeFakeSpan(state, { throwOnSetAttributes: true });
240
+
241
+ expect((): void => {
242
+ Telemetry.recordExceptionMarkSpanAsErrorAndEndSpan({
243
+ span,
244
+ exception: new Error("kaboom"),
245
+ });
246
+ }).not.toThrow();
247
+
248
+ expect(state.ended).toBe(1);
249
+ expect(state.status?.code).toBe(SpanStatusCode.ERROR);
250
+ });
251
+ });
@@ -16,10 +16,15 @@ export default interface DashboardTraceChartComponent extends BaseComponent {
16
16
  // Substring filter on span name (e.g. "/Shipment/ShipShipment").
17
17
  spanNameContains?: string | undefined;
18
18
  /*
19
- * Attribute equality filters, ANDed "key=value; key2=value2"
20
- * (e.g. "url.host=torginol.starship.online").
19
+ * Attribute equality filters, ANDed. The structured editor stores a
20
+ * key/value record (e.g. { "url.host": "torginol.starship.online" }).
21
+ * A legacy "key=value; key2=value2" string is still read for widgets
22
+ * saved before the structured editor existed.
21
23
  */
22
- attributeFilters?: string | undefined;
24
+ attributeFilters?:
25
+ | string
26
+ | Record<string, string | number | boolean>
27
+ | undefined;
23
28
  /*
24
29
  * Optional split dimension: a span attribute key (e.g. url.host,
25
30
  * resource.service.instance.id) or a top-level column (name,
@@ -4,6 +4,7 @@ import React, {
4
4
  useState,
5
5
  useEffect,
6
6
  } from "react";
7
+ import { createPortal } from "react-dom";
7
8
  import Route from "../../../Types/API/Route";
8
9
  import URL from "../../../Types/API/URL";
9
10
  import IconProp from "../../../Types/Icon/IconProp";
@@ -368,9 +369,18 @@ const Navbar: FunctionComponent<ComponentProps> = (
368
369
  </div>
369
370
  )}
370
371
 
372
+ {/*
373
+ * Render the full-screen products modal in a portal on document.body so it
374
+ * is not a flex child of this `justify-between` nav. Otherwise it counts as
375
+ * a third flex item and shifts the right-side user/settings menu toward the
376
+ * middle whenever the modal opens. The modal is `fixed inset-0`, so its
377
+ * visual position is identical when portaled.
378
+ */}
371
379
  {isMoreMenuVisible &&
372
380
  props.moreMenuItems &&
373
- props.moreMenuItems.length > 0 && (
381
+ props.moreMenuItems.length > 0 &&
382
+ typeof document !== "undefined" &&
383
+ createPortal(
374
384
  <NavBarMenuModal
375
385
  items={props.moreMenuItems}
376
386
  footer={props.moreMenuFooter}
@@ -379,7 +389,8 @@ const Navbar: FunctionComponent<ComponentProps> = (
379
389
  keyboardHint={props.moreMenuKeyboardHint}
380
390
  recentLabel={props.moreMenuRecentLabel}
381
391
  onClose={closeMoreMenu}
382
- />
392
+ />,
393
+ document.body,
383
394
  )}
384
395
  </nav>
385
396
  );
@@ -15,11 +15,12 @@ const DisplaySection: ComponentArgumentSection = {
15
15
  order: 1,
16
16
  };
17
17
 
18
- const QuerySection: ComponentArgumentSection = {
19
- name: "Query",
20
- description: "Which spans to aggregate and how to split them",
21
- order: 2,
22
- };
18
+ /*
19
+ * The "Query" section (span-name filter, attribute filters, split-by, max
20
+ * series, include-child-spans) is rendered by a bespoke, structured editor —
21
+ * TraceChartQueryEditor — rather than these declarative free-text fields, so
22
+ * those arguments are deliberately not listed here. See ArgumentsForm.tsx.
23
+ */
23
24
 
24
25
  export default class DashboardTraceChartComponentUtil extends DashboardBaseComponentUtil {
25
26
  public static override getDefaultComponent(): DashboardTraceChartComponent {
@@ -76,59 +77,6 @@ export default class DashboardTraceChartComponentUtil extends DashboardBaseCompo
76
77
  ],
77
78
  });
78
79
 
79
- componentArguments.push({
80
- name: "Span Name Contains",
81
- description:
82
- "Only include spans whose name contains this text (e.g. /Shipment/ShipShipment)",
83
- required: false,
84
- type: ComponentInputType.Text,
85
- id: "spanNameContains",
86
- placeholder: "/Shipment/ShipShipment",
87
- section: QuerySection,
88
- });
89
-
90
- componentArguments.push({
91
- name: "Attribute Filters",
92
- description:
93
- "Attribute equality filters, ANDed — key=value pairs separated by semicolons (e.g. url.host=torginol.starship.online; http.method=POST)",
94
- required: false,
95
- type: ComponentInputType.Text,
96
- id: "attributeFilters",
97
- placeholder: "url.host=torginol.starship.online",
98
- section: QuerySection,
99
- });
100
-
101
- componentArguments.push({
102
- name: "Split By",
103
- description:
104
- "Optional dimension — a span attribute key (e.g. url.host, resource.service.instance.id) or one of: name, primaryEntityId, statusCode, kind. One series per value.",
105
- required: false,
106
- type: ComponentInputType.Text,
107
- id: "groupByAttribute",
108
- placeholder: "url.host",
109
- section: QuerySection,
110
- });
111
-
112
- componentArguments.push({
113
- name: "Top Series",
114
- description: "Cap on the number of series when split (default 10)",
115
- required: false,
116
- type: ComponentInputType.Number,
117
- id: "topLimit",
118
- placeholder: "10",
119
- section: QuerySection,
120
- });
121
-
122
- componentArguments.push({
123
- name: "Include Child Spans",
124
- description:
125
- "Count every span instead of root spans only (the traces explorer defaults to root spans)",
126
- required: false,
127
- type: ComponentInputType.Boolean,
128
- id: "includeChildSpans",
129
- section: QuerySection,
130
- });
131
-
132
80
  return componentArguments;
133
81
  }
134
82
  }