@oneuptime/common 10.0.86 → 10.0.89

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 (126) hide show
  1. package/Models/DatabaseModels/EnterpriseLicense.ts +54 -0
  2. package/Models/DatabaseModels/GlobalConfig.ts +51 -0
  3. package/Server/API/EnterpriseLicenseAPI.ts +83 -0
  4. package/Server/API/GlobalConfigAPI.ts +59 -0
  5. package/Server/API/MetricAPI.ts +149 -0
  6. package/Server/API/TelemetryAPI.ts +24 -0
  7. package/Server/EnvironmentConfig.ts +10 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.ts +59 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  10. package/Server/Infrastructure/Queue.ts +4 -4
  11. package/Server/Services/AnalyticsDatabaseService.ts +21 -0
  12. package/Server/Services/MetricService.ts +193 -1
  13. package/Server/Services/TelemetryAttributeService.ts +37 -3
  14. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +174 -7
  15. package/Tests/Types/Date.test.ts +46 -0
  16. package/Types/Dashboard/DashboardComponentType.ts +3 -0
  17. package/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.ts +13 -0
  18. package/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.ts +13 -0
  19. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +13 -0
  20. package/Types/Date.ts +9 -4
  21. package/Types/JSONFunctions.ts +61 -1
  22. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +60 -21
  23. package/UI/Components/Dictionary/Dictionary.tsx +188 -26
  24. package/UI/Components/Dictionary/DictionaryFilterOperator.ts +357 -0
  25. package/UI/Components/Dictionary/DictionaryOfStrings.tsx +12 -7
  26. package/UI/Components/EditionLabel/EditionLabel.tsx +224 -10
  27. package/UI/Components/Filters/FilterViewer.tsx +81 -16
  28. package/UI/Components/Filters/FiltersForm.tsx +18 -3
  29. package/UI/Components/Filters/JSONFilter.tsx +11 -2
  30. package/UI/Components/Filters/Types/Filter.ts +3 -0
  31. package/UI/Components/Forms/Fields/FormField.tsx +6 -1
  32. package/UI/Components/Forms/Types/Field.ts +5 -0
  33. package/UI/Components/LogsViewer/LogsViewer.tsx +73 -4
  34. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +77 -31
  35. package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +44 -1
  36. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +7 -5
  37. package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +6 -0
  38. package/UI/Components/TelemetryViewer/components/TelemetrySearchBar.tsx +84 -25
  39. package/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.tsx +44 -1
  40. package/Utils/Dashboard/Components/DashboardAlertListComponent.ts +86 -0
  41. package/Utils/Dashboard/Components/DashboardIncidentListComponent.ts +86 -0
  42. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +85 -0
  43. package/Utils/Dashboard/Components/Index.ts +21 -0
  44. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +57 -0
  45. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  46. package/build/dist/Models/DatabaseModels/GlobalConfig.js +54 -0
  47. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  48. package/build/dist/Server/API/EnterpriseLicenseAPI.js +64 -1
  49. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  50. package/build/dist/Server/API/GlobalConfigAPI.js +47 -0
  51. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  52. package/build/dist/Server/API/MetricAPI.js +123 -0
  53. package/build/dist/Server/API/MetricAPI.js.map +1 -0
  54. package/build/dist/Server/API/TelemetryAPI.js +9 -0
  55. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  56. package/build/dist/Server/EnvironmentConfig.js +3 -0
  57. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js +26 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1777629313843-MigrationName.js.map +1 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  62. package/build/dist/Server/Infrastructure/Queue.js +3 -3
  63. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  64. package/build/dist/Server/Services/AnalyticsDatabaseService.js +18 -0
  65. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  66. package/build/dist/Server/Services/MetricService.js +151 -1
  67. package/build/dist/Server/Services/MetricService.js.map +1 -1
  68. package/build/dist/Server/Services/TelemetryAttributeService.js +36 -7
  69. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  70. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +135 -5
  71. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  72. package/build/dist/Tests/Types/Date.test.js +40 -0
  73. package/build/dist/Tests/Types/Date.test.js.map +1 -1
  74. package/build/dist/Types/Dashboard/DashboardComponentType.js +3 -0
  75. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  76. package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js +2 -0
  77. package/build/dist/Types/Dashboard/DashboardComponents/DashboardAlertListComponent.js.map +1 -0
  78. package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js +2 -0
  79. package/build/dist/Types/Dashboard/DashboardComponents/DashboardIncidentListComponent.js.map +1 -0
  80. package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js +2 -0
  81. package/build/dist/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.js.map +1 -0
  82. package/build/dist/Types/Date.js +7 -2
  83. package/build/dist/Types/Date.js.map +1 -1
  84. package/build/dist/Types/JSONFunctions.js +47 -1
  85. package/build/dist/Types/JSONFunctions.js.map +1 -1
  86. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +21 -10
  87. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  88. package/build/dist/UI/Components/Dictionary/Dictionary.js +109 -16
  89. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  90. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +263 -0
  91. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -0
  92. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js +10 -6
  93. package/build/dist/UI/Components/Dictionary/DictionaryOfStrings.js.map +1 -1
  94. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +124 -6
  95. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  96. package/build/dist/UI/Components/Filters/FilterViewer.js +50 -12
  97. package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
  98. package/build/dist/UI/Components/Filters/FiltersForm.js +5 -4
  99. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  100. package/build/dist/UI/Components/Filters/JSONFilter.js +1 -1
  101. package/build/dist/UI/Components/Filters/JSONFilter.js.map +1 -1
  102. package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
  103. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  104. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +54 -5
  105. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  106. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +59 -29
  107. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  108. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +10 -2
  109. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -1
  110. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +2 -5
  111. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  112. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
  113. package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
  114. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js +59 -22
  115. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchBar.js.map +1 -1
  116. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js +10 -2
  117. package/build/dist/UI/Components/TelemetryViewer/components/TelemetrySearchSuggestions.js.map +1 -1
  118. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js +70 -0
  119. package/build/dist/Utils/Dashboard/Components/DashboardAlertListComponent.js.map +1 -0
  120. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js +70 -0
  121. package/build/dist/Utils/Dashboard/Components/DashboardIncidentListComponent.js.map +1 -0
  122. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +69 -0
  123. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -0
  124. package/build/dist/Utils/Dashboard/Components/Index.js +12 -0
  125. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  126. package/package.json +1 -1
@@ -5,10 +5,11 @@ import AggregateBy, {
5
5
  AggregateUtil,
6
6
  } from "../Types/AnalyticsDatabase/AggregateBy";
7
7
  import { SQL, Statement } from "../Utils/AnalyticsDatabase/Statement";
8
- import {
8
+ import AggregationType, {
9
9
  getPercentileLevel,
10
10
  isPercentileAggregation,
11
11
  } from "../../Types/BaseDatabase/AggregationType";
12
+ import AggregationInterval from "../../Types/BaseDatabase/AggregationInterval";
12
13
  import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
13
14
  import logger, { LogAttributes } from "../Utils/Logger";
14
15
 
@@ -56,6 +57,13 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
56
57
  columns: Array<string>;
57
58
  } {
58
59
  if (!isPercentileAggregation(aggregateBy.aggregationType)) {
60
+ const mvStatement: {
61
+ statement: Statement;
62
+ columns: Array<string>;
63
+ } | null = this.tryBuildMinuteAggregateMVStatement(aggregateBy);
64
+ if (mvStatement) {
65
+ return mvStatement;
66
+ }
59
67
  return super.toAggregateStatement(aggregateBy);
60
68
  }
61
69
 
@@ -216,6 +224,16 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
216
224
  }} `,
217
225
  );
218
226
 
227
+ /*
228
+ * Match the read-path settings the base aggregator now appends (see
229
+ * AnalyticsDatabaseService.toAggregateStatement). The percentile
230
+ * path bypasses the base method, so we mirror them here to keep
231
+ * cluster behavior consistent across aggregation kinds.
232
+ */
233
+ statement.append(
234
+ ` SETTINGS optimize_aggregation_in_order=1, optimize_move_to_prewhere=1, max_threads=4`,
235
+ );
236
+
219
237
  const columns: Array<string> = [
220
238
  aggregationColumn,
221
239
  aggregationTimestampColumn,
@@ -231,6 +249,180 @@ export class MetricService extends AnalyticsDatabaseService<Metric> {
231
249
 
232
250
  return { statement, columns };
233
251
  }
252
+
253
+ /*
254
+ * Materialized-view fast path for scalar aggregations.
255
+ *
256
+ * Returns a statement that reads from MetricItemAggMV1m (the
257
+ * 1-minute pre-aggregate created by
258
+ * AddMetricMinuteAggregateMaterializedView) when:
259
+ *
260
+ * - The aggregation is Sum/Avg/Min/Max/Count over `value`.
261
+ * - The dashboard's effective bucket interval is >= 1 minute (the
262
+ * MV stores 1-minute states; sub-minute requests need raw rows).
263
+ * - The query carries no per-attribute filter or group-by, since
264
+ * the MV is keyed by (projectId, name, serviceId, bucketTime)
265
+ * only — it does not preserve attribute breakdowns.
266
+ * - The query carries no group-by other than the time bucket.
267
+ *
268
+ * Returns `null` if any condition fails so the caller falls back to
269
+ * the base table. The result row shape (columns: aggregateColumn,
270
+ * timestampColumn) matches the base statement so downstream code
271
+ * needs no changes.
272
+ */
273
+ private tryBuildMinuteAggregateMVStatement(
274
+ aggregateBy: AggregateBy<Metric>,
275
+ ): { statement: Statement; columns: Array<string> } | null {
276
+ const aggType: AggregationType = aggregateBy.aggregationType;
277
+ const supported: ReadonlyArray<AggregationType> = [
278
+ AggregationType.Sum,
279
+ AggregationType.Avg,
280
+ AggregationType.Min,
281
+ AggregationType.Max,
282
+ AggregationType.Count,
283
+ ];
284
+ if (!supported.includes(aggType)) {
285
+ return null;
286
+ }
287
+
288
+ if (
289
+ aggregateBy.aggregateColumnName.toString() !== "value" ||
290
+ aggregateBy.aggregationTimestampColumnName.toString() !== "time"
291
+ ) {
292
+ return null;
293
+ }
294
+
295
+ const interval: AggregationInterval = AggregateUtil.getAggregationInterval({
296
+ startDate: aggregateBy.startTimestamp!,
297
+ endDate: aggregateBy.endTimestamp!,
298
+ });
299
+ /*
300
+ * The MV is bucketed at 1 minute, so all values of AggregationInterval
301
+ * (Minute / Hour / Day / Week / Month / Year) are >= MV resolution
302
+ * and acceptable. Kept as a no-op read so the dependency on
303
+ * AggregateUtil makes the intent obvious.
304
+ */
305
+ void interval;
306
+
307
+ const queryRecord: Record<string, unknown> =
308
+ (aggregateBy.query as unknown as Record<string, unknown>) || {};
309
+ const attrs: unknown = queryRecord["attributes"];
310
+ if (
311
+ attrs !== undefined &&
312
+ attrs !== null &&
313
+ !(
314
+ typeof attrs === "object" &&
315
+ Object.keys(attrs as Record<string, unknown>).length === 0
316
+ )
317
+ ) {
318
+ return null;
319
+ }
320
+
321
+ if (aggregateBy.groupBy && Object.keys(aggregateBy.groupBy).length > 0) {
322
+ return null;
323
+ }
324
+
325
+ if (!this.database) {
326
+ this.useDefaultDatabase();
327
+ }
328
+ const databaseName: string = this.database.getDatasourceOptions().database!;
329
+
330
+ const intervalLower: string = interval.toLowerCase();
331
+
332
+ let mergedExpr: string;
333
+ if (aggType === AggregationType.Sum) {
334
+ mergedExpr = `sumMerge(valueSumState)`;
335
+ } else if (aggType === AggregationType.Count) {
336
+ mergedExpr = `countMerge(valueCountState)`;
337
+ } else if (aggType === AggregationType.Min) {
338
+ mergedExpr = `minMerge(valueMinState)`;
339
+ } else if (aggType === AggregationType.Max) {
340
+ mergedExpr = `maxMerge(valueMaxState)`;
341
+ } else {
342
+ // Avg = sum / count, derived from the two stored states.
343
+ mergedExpr = `if(countMerge(valueCountState) = 0, 0, sumMerge(valueSumState) / countMerge(valueCountState))`;
344
+ }
345
+
346
+ /*
347
+ * Build the WHERE on a copy of the query with `time` removed so
348
+ * the generator never references a column that doesn't exist on
349
+ * the MV. We then add an explicit `bucketTime` range from
350
+ * startTimestamp/endTimestamp.
351
+ */
352
+ const nonTimeWhere: Statement = this.statementGenerator.toWhereStatement(
353
+ this.stripTimeFromQuery(aggregateBy.query) as typeof aggregateBy.query,
354
+ );
355
+ const sortStatement: Statement = this.statementGenerator.toSortStatement(
356
+ aggregateBy.sort!,
357
+ );
358
+
359
+ const statement: Statement = SQL``;
360
+
361
+ statement.append(
362
+ `SELECT ${mergedExpr} as value, date_trunc('${intervalLower}', toStartOfInterval(bucketTime, INTERVAL 1 ${intervalLower})) as time`,
363
+ );
364
+ statement.append(SQL` FROM ${databaseName}.MetricItemAggMV1m`);
365
+ statement.append(
366
+ ` WHERE bucketTime >= toDateTime('${this.formatDateTime(aggregateBy.startTimestamp!)}') AND bucketTime <= toDateTime('${this.formatDateTime(aggregateBy.endTimestamp!)}')`,
367
+ );
368
+ statement.append(SQL` `).append(nonTimeWhere);
369
+
370
+ statement.append(SQL` GROUP BY `).append(`time`);
371
+ statement.append(SQL` ORDER BY `).append(sortStatement);
372
+ statement.append(
373
+ SQL` LIMIT ${{
374
+ value: Number(aggregateBy.limit),
375
+ type: TableColumnType.Number,
376
+ }}`,
377
+ );
378
+ statement.append(
379
+ SQL` OFFSET ${{
380
+ value: Number(aggregateBy.skip),
381
+ type: TableColumnType.Number,
382
+ }} `,
383
+ );
384
+ statement.append(
385
+ ` SETTINGS optimize_aggregation_in_order=1, optimize_move_to_prewhere=1, max_threads=4`,
386
+ );
387
+
388
+ logger.debug(`${this.model.tableName} MV Aggregate Statement`, {
389
+ tableName: this.model.tableName,
390
+ } as LogAttributes);
391
+ logger.debug(statement, {
392
+ tableName: this.model.tableName,
393
+ } as LogAttributes);
394
+
395
+ return {
396
+ statement,
397
+ columns: [
398
+ aggregateBy.aggregateColumnName.toString(),
399
+ aggregateBy.aggregationTimestampColumnName.toString(),
400
+ ],
401
+ };
402
+ }
403
+
404
+ private stripTimeFromQuery(query: unknown): typeof query {
405
+ if (!query || typeof query !== "object") {
406
+ return query;
407
+ }
408
+ const out: Record<string, unknown> = {};
409
+ for (const [k, v] of Object.entries(query as Record<string, unknown>)) {
410
+ if (k === "time") {
411
+ continue;
412
+ }
413
+ out[k] = v;
414
+ }
415
+ return out as typeof query;
416
+ }
417
+
418
+ private formatDateTime(d: Date): string {
419
+ /*
420
+ * ClickHouse's DateTime parser accepts 'YYYY-MM-DD HH:MM:SS'.
421
+ * toISOString gives 'YYYY-MM-DDTHH:MM:SS.sssZ'; trim the milliseconds
422
+ * and the trailing 'Z' and replace 'T' with a space.
423
+ */
424
+ return new Date(d).toISOString().replace("T", " ").substring(0, 19);
425
+ }
234
426
  }
235
427
 
236
428
  export default new MetricService();
@@ -3,6 +3,7 @@ import TelemetryType from "../../Types/Telemetry/TelemetryType";
3
3
  import LogDatabaseService from "./LogService";
4
4
  import MetricDatabaseService from "./MetricService";
5
5
  import SpanDatabaseService from "./SpanService";
6
+ import ExceptionInstanceService from "./ExceptionInstanceService";
6
7
  import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
7
8
  import { JSONObject } from "../../Types/JSON";
8
9
  import ObjectID from "../../Types/ObjectID";
@@ -18,7 +19,12 @@ type TelemetrySource = {
18
19
  service: AnalyticsDatabaseService<any>;
19
20
  tableName: string;
20
21
  attributesColumn: string;
21
- attributeKeysColumn: string;
22
+ /*
23
+ * Some tables (e.g. ExceptionInstance) don't have a separate
24
+ * attributeKeys array column — only the attributes map. Leave this
25
+ * undefined for those; the SQL falls back to mapKeys(attributes).
26
+ */
27
+ attributeKeysColumn?: string | undefined;
22
28
  timeColumn: string;
23
29
  };
24
30
 
@@ -62,6 +68,14 @@ export class TelemetryAttributeService {
62
68
  attributeKeysColumn: "attributeKeys",
63
69
  timeColumn: "startTime",
64
70
  };
71
+ case TelemetryType.Exception:
72
+ return {
73
+ service: ExceptionInstanceService,
74
+ tableName: ExceptionInstanceService.model.tableName,
75
+ attributesColumn: "attributes",
76
+ // ExceptionInstance has no attributeKeys column.
77
+ timeColumn: "time",
78
+ };
65
79
  default:
66
80
  return null;
67
81
  }
@@ -225,14 +239,21 @@ export class TelemetryAttributeService {
225
239
  projectId: ObjectID;
226
240
  tableName: string;
227
241
  attributesColumn: string;
228
- attributeKeysColumn: string;
242
+ attributeKeysColumn?: string | undefined;
229
243
  timeColumn: string;
230
244
  metricName?: string | undefined;
231
245
  }): Statement {
232
246
  const lookbackStartDate: Date =
233
247
  TelemetryAttributeService.getLookbackStartDate();
234
248
 
235
- const statement: Statement = SQL`
249
+ /*
250
+ * If the source has a denormalized attributeKeys array column, prefer it
251
+ * (avoids materializing every row's map). Otherwise fall back to
252
+ * mapKeys(attributes) — slower but works for tables that don't carry
253
+ * the precomputed array (e.g. ExceptionInstance).
254
+ */
255
+ const statement: Statement = data.attributeKeysColumn
256
+ ? SQL`
236
257
  WITH filtered AS (
237
258
  SELECT arrayJoin(
238
259
  if(
@@ -250,6 +271,19 @@ export class TelemetryAttributeService {
250
271
  NOT empty(${data.attributeKeysColumn}) OR
251
272
  NOT empty(${data.attributesColumn})
252
273
  )
274
+ AND ${data.timeColumn} >= ${{
275
+ type: TableColumnType.Date,
276
+ value: lookbackStartDate,
277
+ }}`
278
+ : SQL`
279
+ WITH filtered AS (
280
+ SELECT arrayJoin(mapKeys(${data.attributesColumn})) AS attribute
281
+ FROM ${data.tableName}
282
+ WHERE projectId = ${{
283
+ type: TableColumnType.ObjectID,
284
+ value: data.projectId,
285
+ }}
286
+ AND NOT empty(${data.attributesColumn})
253
287
  AND ${data.timeColumn} >= ${{
254
288
  type: TableColumnType.Date,
255
289
  value: lookbackStartDate,
@@ -15,6 +15,7 @@ import AnalyticsTableColumn, {
15
15
  SkipIndexType,
16
16
  } from "../../../Types/AnalyticsDatabase/TableColumn";
17
17
  import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
18
+ import EqualTo from "../../../Types/BaseDatabase/EqualTo";
18
19
  import GreaterThan from "../../../Types/BaseDatabase/GreaterThan";
19
20
  import GreaterThanOrEqual from "../../../Types/BaseDatabase/GreaterThanOrEqual";
20
21
  import InBetween from "../../../Types/BaseDatabase/InBetween";
@@ -25,7 +26,11 @@ import LessThanOrEqual from "../../../Types/BaseDatabase/LessThanOrEqual";
25
26
  import GreaterThanOrNull from "../../../Types/BaseDatabase/GreaterThanOrNull";
26
27
  import LessThanOrNull from "../../../Types/BaseDatabase/LessThanOrNull";
27
28
  import NotEqual from "../../../Types/BaseDatabase/NotEqual";
29
+ import NotContains from "../../../Types/BaseDatabase/NotContains";
30
+ import NotNull from "../../../Types/BaseDatabase/NotNull";
28
31
  import Search from "../../../Types/BaseDatabase/Search";
32
+ import StartsWith from "../../../Types/BaseDatabase/StartsWith";
33
+ import EndsWith from "../../../Types/BaseDatabase/EndsWith";
29
34
  import SortOrder from "../../../Types/BaseDatabase/SortOrder";
30
35
  import OneUptimeDate from "../../../Types/Date";
31
36
  import BadDataException from "../../../Types/Exception/BadDataException";
@@ -447,33 +452,195 @@ export default class StatementGenerator<TBaseModel extends AnalyticsBaseModel> {
447
452
  tableColumn.type === TableColumnType.MapStringString &&
448
453
  typeof value === "object"
449
454
  ) {
450
- const mapValue: Record<string, string | Search<string>> =
451
- value as Record<string, string | Search<string>>;
455
+ const mapValue: Record<string, unknown> = value as Record<
456
+ string,
457
+ unknown
458
+ >;
452
459
  for (const mapKey in mapValue) {
453
- if (mapValue[mapKey] === undefined) {
460
+ const mapEntry: unknown = mapValue[mapKey];
461
+ if (mapEntry === undefined || mapEntry === null) {
454
462
  continue;
455
463
  }
456
- if (mapValue[mapKey] instanceof Search) {
464
+
465
+ /*
466
+ * ClickHouse Map columns return the value type's default for
467
+ * missing keys (empty string for String values), so to express
468
+ * "is empty" we have to cover both the missing-key and the
469
+ * empty-string case explicitly.
470
+ */
471
+ if (mapEntry instanceof IsNull) {
472
+ whereStatement.append(
473
+ SQL`AND ((NOT mapContains(${key}, ${{
474
+ value: mapKey,
475
+ type: TableColumnType.Text,
476
+ }})) OR ${key}[${{
477
+ value: mapKey,
478
+ type: TableColumnType.Text,
479
+ }}] = '')`,
480
+ );
481
+ continue;
482
+ }
483
+
484
+ if (mapEntry instanceof NotNull) {
485
+ whereStatement.append(
486
+ SQL`AND mapContains(${key}, ${{
487
+ value: mapKey,
488
+ type: TableColumnType.Text,
489
+ }}) AND ${key}[${{
490
+ value: mapKey,
491
+ type: TableColumnType.Text,
492
+ }}] != ''`,
493
+ );
494
+ continue;
495
+ }
496
+
497
+ if (mapEntry instanceof Search) {
498
+ whereStatement.append(
499
+ SQL`AND ${key}[${{
500
+ value: mapKey,
501
+ type: TableColumnType.Text,
502
+ }}] ILIKE ${{
503
+ value: mapEntry as Search<string>,
504
+ type: TableColumnType.Text,
505
+ }}`,
506
+ );
507
+ continue;
508
+ }
509
+
510
+ if (mapEntry instanceof NotContains) {
511
+ const literalValue: string = `%${(mapEntry.value as string) || ""}%`;
512
+ whereStatement.append(
513
+ SQL`AND ${key}[${{
514
+ value: mapKey,
515
+ type: TableColumnType.Text,
516
+ }}] NOT ILIKE ${{
517
+ value: literalValue,
518
+ type: TableColumnType.Text,
519
+ }}`,
520
+ );
521
+ continue;
522
+ }
523
+
524
+ if (mapEntry instanceof StartsWith) {
525
+ const literalValue: string = `${(mapEntry.value as string) || ""}%`;
526
+ whereStatement.append(
527
+ SQL`AND ${key}[${{
528
+ value: mapKey,
529
+ type: TableColumnType.Text,
530
+ }}] ILIKE ${{
531
+ value: literalValue,
532
+ type: TableColumnType.Text,
533
+ }}`,
534
+ );
535
+ continue;
536
+ }
537
+
538
+ if (mapEntry instanceof EndsWith) {
539
+ const literalValue: string = `%${(mapEntry.value as string) || ""}`;
457
540
  whereStatement.append(
458
541
  SQL`AND ${key}[${{
459
542
  value: mapKey,
460
543
  type: TableColumnType.Text,
461
544
  }}] ILIKE ${{
462
- value: mapValue[mapKey] as Search<string>,
545
+ value: literalValue,
463
546
  type: TableColumnType.Text,
464
547
  }}`,
465
548
  );
466
- } else {
549
+ continue;
550
+ }
551
+
552
+ if (mapEntry instanceof NotEqual) {
553
+ whereStatement.append(
554
+ SQL`AND ${key}[${{
555
+ value: mapKey,
556
+ type: TableColumnType.Text,
557
+ }}] != ${{
558
+ value: String((mapEntry as NotEqual<string>).value ?? ""),
559
+ type: TableColumnType.Text,
560
+ }}`,
561
+ );
562
+ continue;
563
+ }
564
+
565
+ if (mapEntry instanceof EqualTo) {
467
566
  whereStatement.append(
468
567
  SQL`AND ${key}[${{
469
568
  value: mapKey,
470
569
  type: TableColumnType.Text,
471
570
  }}] = ${{
472
- value: mapValue[mapKey] as string,
571
+ value: String((mapEntry as EqualTo<any>).value ?? ""),
473
572
  type: TableColumnType.Text,
474
573
  }}`,
475
574
  );
575
+ continue;
476
576
  }
577
+
578
+ /*
579
+ * Map values are stored as text; cast to Float64 for numeric
580
+ * comparisons and skip rows where the cast fails (non-numeric).
581
+ */
582
+ if (mapEntry instanceof GreaterThan) {
583
+ whereStatement.append(
584
+ SQL`AND toFloat64OrNull(${key}[${{
585
+ value: mapKey,
586
+ type: TableColumnType.Text,
587
+ }}]) > ${{
588
+ value: Number((mapEntry as GreaterThan<any>).value),
589
+ type: TableColumnType.Number,
590
+ }}`,
591
+ );
592
+ continue;
593
+ }
594
+
595
+ if (mapEntry instanceof GreaterThanOrEqual) {
596
+ whereStatement.append(
597
+ SQL`AND toFloat64OrNull(${key}[${{
598
+ value: mapKey,
599
+ type: TableColumnType.Text,
600
+ }}]) >= ${{
601
+ value: Number((mapEntry as GreaterThanOrEqual<any>).value),
602
+ type: TableColumnType.Number,
603
+ }}`,
604
+ );
605
+ continue;
606
+ }
607
+
608
+ if (mapEntry instanceof LessThan) {
609
+ whereStatement.append(
610
+ SQL`AND toFloat64OrNull(${key}[${{
611
+ value: mapKey,
612
+ type: TableColumnType.Text,
613
+ }}]) < ${{
614
+ value: Number((mapEntry as LessThan<any>).value),
615
+ type: TableColumnType.Number,
616
+ }}`,
617
+ );
618
+ continue;
619
+ }
620
+
621
+ if (mapEntry instanceof LessThanOrEqual) {
622
+ whereStatement.append(
623
+ SQL`AND toFloat64OrNull(${key}[${{
624
+ value: mapKey,
625
+ type: TableColumnType.Text,
626
+ }}]) <= ${{
627
+ value: Number((mapEntry as LessThanOrEqual<any>).value),
628
+ type: TableColumnType.Number,
629
+ }}`,
630
+ );
631
+ continue;
632
+ }
633
+
634
+ // Bare string/number/boolean — back-compat with existing data.
635
+ whereStatement.append(
636
+ SQL`AND ${key}[${{
637
+ value: mapKey,
638
+ type: TableColumnType.Text,
639
+ }}] = ${{
640
+ value: String(mapEntry),
641
+ type: TableColumnType.Text,
642
+ }}`,
643
+ );
477
644
  }
478
645
  } else if (
479
646
  (tableColumn.type === TableColumnType.JSON ||
@@ -250,4 +250,50 @@ describe("class OneUptimeDate", () => {
250
250
  expect(result[0]).not.toContain("EST");
251
251
  });
252
252
  });
253
+
254
+ describe("getDateAsFormattedArrayInMultipleTimezones default timezones", () => {
255
+ test("defaults include UTC, London, New York, LA, Kolkata, Sydney", () => {
256
+ const result: Array<string> =
257
+ OneUptimeDate.getDateAsFormattedArrayInMultipleTimezones({
258
+ date: new Date("2026-07-15T17:00:00Z"),
259
+ use12HourFormat: true,
260
+ });
261
+ expect(result).toHaveLength(6);
262
+ expect(result[0]).toContain("UTC");
263
+ });
264
+
265
+ test("summer (DST) event shows EDT and BST in defaults, not EST or GMT", () => {
266
+ /*
267
+ * Apr 27 2026 21:30 UTC — both US and UK are in DST. Use word
268
+ * boundaries so AEST/AEDT (Sydney) doesn't match EST/EDT.
269
+ */
270
+ const result: Array<string> =
271
+ OneUptimeDate.getDateAsFormattedArrayInMultipleTimezones({
272
+ date: new Date("2026-04-27T21:30:00Z"),
273
+ use12HourFormat: true,
274
+ });
275
+ const joined: string = result.join("\n");
276
+ expect(joined).toMatch(/\bEDT\b/);
277
+ expect(joined).toMatch(/\bBST\b/);
278
+ expect(joined).not.toMatch(/\bEST\b/);
279
+ expect(joined).not.toMatch(/\bGMT\b/);
280
+ });
281
+
282
+ test("winter (no DST) event shows EST and GMT in defaults, not EDT or BST", () => {
283
+ /*
284
+ * Jan 15 2026 17:00 UTC — both US and UK are on standard time. Use
285
+ * word boundaries so AEDT (Sydney) doesn't match EDT.
286
+ */
287
+ const result: Array<string> =
288
+ OneUptimeDate.getDateAsFormattedArrayInMultipleTimezones({
289
+ date: new Date("2026-01-15T17:00:00Z"),
290
+ use12HourFormat: true,
291
+ });
292
+ const joined: string = result.join("\n");
293
+ expect(joined).toMatch(/\bEST\b/);
294
+ expect(joined).toMatch(/\bGMT\b/);
295
+ expect(joined).not.toMatch(/\bEDT\b/);
296
+ expect(joined).not.toMatch(/\bBST\b/);
297
+ });
298
+ });
253
299
  });
@@ -6,6 +6,9 @@ enum DashboardComponentType {
6
6
  Gauge = `Gauge`,
7
7
  LogStream = `LogStream`,
8
8
  TraceList = `TraceList`,
9
+ IncidentList = `IncidentList`,
10
+ AlertList = `AlertList`,
11
+ MonitorList = `MonitorList`,
9
12
  }
10
13
 
11
14
  export default DashboardComponentType;
@@ -0,0 +1,13 @@
1
+ import ObjectID from "../../ObjectID";
2
+ import DashboardComponentType from "../DashboardComponentType";
3
+ import BaseComponent from "./DashboardBaseComponent";
4
+
5
+ export default interface DashboardAlertListComponent extends BaseComponent {
6
+ componentType: DashboardComponentType.AlertList;
7
+ componentId: ObjectID;
8
+ arguments: {
9
+ title?: string | undefined;
10
+ maxRows?: number | undefined;
11
+ stateFilter?: string | undefined;
12
+ };
13
+ }
@@ -0,0 +1,13 @@
1
+ import ObjectID from "../../ObjectID";
2
+ import DashboardComponentType from "../DashboardComponentType";
3
+ import BaseComponent from "./DashboardBaseComponent";
4
+
5
+ export default interface DashboardIncidentListComponent extends BaseComponent {
6
+ componentType: DashboardComponentType.IncidentList;
7
+ componentId: ObjectID;
8
+ arguments: {
9
+ title?: string | undefined;
10
+ maxRows?: number | undefined;
11
+ stateFilter?: string | undefined;
12
+ };
13
+ }
@@ -0,0 +1,13 @@
1
+ import ObjectID from "../../ObjectID";
2
+ import DashboardComponentType from "../DashboardComponentType";
3
+ import BaseComponent from "./DashboardBaseComponent";
4
+
5
+ export default interface DashboardMonitorListComponent extends BaseComponent {
6
+ componentType: DashboardComponentType.MonitorList;
7
+ componentId: ObjectID;
8
+ arguments: {
9
+ title?: string | undefined;
10
+ maxRows?: number | undefined;
11
+ statusFilter?: string | undefined;
12
+ };
13
+ }
package/Types/Date.ts CHANGED
@@ -1385,13 +1385,18 @@ export default class OneUptimeDate {
1385
1385
  formatstring = "MMM DD, YYYY";
1386
1386
  }
1387
1387
 
1388
- // convert this date into GMT, EST, PST, IST, ACT with moment
1389
1388
  const timezoneDates: Array<string> = [];
1390
1389
 
1391
1390
  if (!timezones || timezones.length === 0) {
1391
+ /*
1392
+ * Use IANA region zones (not fixed-offset zones like "EST") so the
1393
+ * abbreviation reflects DST at the event's date — e.g. EST/EDT for
1394
+ * America/New_York and GMT/BST for Europe/London.
1395
+ */
1392
1396
  timezones = [
1393
1397
  Timezone.UTC,
1394
- Timezone.EST,
1398
+ Timezone.EuropeLondon,
1399
+ Timezone.AmericaNew_York,
1395
1400
  Timezone.AmericaLos_Angeles,
1396
1401
  Timezone.AsiaKolkata,
1397
1402
  Timezone.AustraliaSydney,
@@ -1416,7 +1421,7 @@ export default class OneUptimeDate {
1416
1421
  public static getDateAsFormattedHTMLInMultipleTimezones(data: {
1417
1422
  date: string | Date;
1418
1423
  onlyShowDate?: boolean;
1419
- timezones?: Array<Timezone> | undefined; // if this is skipped, then it will show the default timezones in the order of UTC, EST, PST, IST, ACT
1424
+ timezones?: Array<Timezone> | undefined; // if skipped, defaults to UTC, Europe/London, America/New_York, America/Los_Angeles, Asia/Kolkata, Australia/Sydney (DST-aware)
1420
1425
  use12HourFormat?: boolean | undefined;
1421
1426
  }): string {
1422
1427
  const date: string | Date = data.date;
@@ -1435,7 +1440,7 @@ export default class OneUptimeDate {
1435
1440
  public static getDateAsFormattedStringInMultipleTimezones(data: {
1436
1441
  date: string | Date;
1437
1442
  onlyShowDate?: boolean | undefined;
1438
- timezones?: Array<Timezone> | undefined; // if this is skipped, then it will show the default timezones in the order of UTC, EST, PST, IST, ACT
1443
+ timezones?: Array<Timezone> | undefined; // if skipped, defaults to UTC, Europe/London, America/New_York, America/Los_Angeles, Asia/Kolkata, Australia/Sydney (DST-aware)
1439
1444
  use12HourFormat?: boolean | undefined;
1440
1445
  }): string {
1441
1446
  const date: string | Date = data.date;