@oneuptime/common 12.0.20 → 12.0.21

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 (86) hide show
  1. package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
  2. package/Models/AnalyticsModels/Index.ts +2 -0
  3. package/Server/EnvironmentConfig.ts +1 -8
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
  6. package/Server/Services/ChangeEventService.ts +11 -0
  7. package/Server/Services/EnterpriseLicenseService.ts +3 -3
  8. package/Server/Services/Index.ts +2 -0
  9. package/Server/Utils/Attribution.ts +11 -11
  10. package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
  11. package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
  12. package/Server/Views/Partials/AnalyticsConsent.ejs +3 -95
  13. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +64 -0
  14. package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
  15. package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
  16. package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
  17. package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
  18. package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
  19. package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
  20. package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
  21. package/Tests/Server/Utils/Attribution.test.ts +4 -5
  22. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
  23. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
  24. package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
  25. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +44 -8
  26. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
  27. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
  28. package/Types/Marketing/Attribution.ts +9 -15
  29. package/Types/Marketing/MarketingEvent.ts +10 -46
  30. package/UI/Components/Charts/Area/AreaChart.tsx +15 -0
  31. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
  32. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +189 -93
  33. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +6 -1
  34. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +213 -114
  35. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +42 -27
  36. package/UI/Components/Charts/Line/LineChart.tsx +15 -0
  37. package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
  38. package/Utils/Telemetry/CrossSignalScope.ts +19 -10
  39. package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
  40. package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
  41. package/build/dist/Models/AnalyticsModels/Index.js +2 -0
  42. package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
  43. package/build/dist/Server/EnvironmentConfig.js +1 -7
  44. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
  48. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  49. package/build/dist/Server/Services/ChangeEventService.js +9 -0
  50. package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
  51. package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
  52. package/build/dist/Server/Services/Index.js +2 -0
  53. package/build/dist/Server/Services/Index.js.map +1 -1
  54. package/build/dist/Server/Utils/Attribution.js +6 -6
  55. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
  56. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
  57. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
  58. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
  59. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
  60. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  61. package/build/dist/Types/Marketing/Attribution.js +9 -15
  62. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  63. package/build/dist/Types/Marketing/MarketingEvent.js +2 -33
  64. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
  65. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  66. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  67. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
  68. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  69. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +92 -38
  70. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  71. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +8 -8
  72. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  73. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +95 -37
  74. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  75. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +39 -24
  76. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -1
  77. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  78. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  79. package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
  80. package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
  81. package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
  82. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  83. package/package.json +1 -1
  84. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
@@ -54,6 +54,24 @@ import ChartCurve from "../../Types/ChartCurve";
54
54
  */
55
55
  const MAX_LABELED_TIME_REFERENCE_LINES: number = 6;
56
56
 
57
+ /*
58
+ * recharts invokes child event handlers with varying shapes ((event) for
59
+ * plain SVG passthrough, (data, event) for adapted children) — find the
60
+ * DOM event wherever it is so propagation can be stopped reliably.
61
+ */
62
+ function stopChartEventPropagation(...args: Array<unknown>): void {
63
+ for (const candidate of args) {
64
+ if (
65
+ candidate &&
66
+ typeof (candidate as { stopPropagation?: unknown }).stopPropagation ===
67
+ "function"
68
+ ) {
69
+ (candidate as { stopPropagation: () => void }).stopPropagation();
70
+ return;
71
+ }
72
+ }
73
+ }
74
+
57
75
  //#region Legend
58
76
 
59
77
  interface LegendItemProps {
@@ -613,6 +631,26 @@ interface AreaChartProps extends React.HTMLAttributes<HTMLDivElement> {
613
631
  formattedExemplarPoints?: Array<FormattedExemplarPoint> | undefined;
614
632
  onExemplarClick?: ((exemplar: ExemplarPoint) => void) | undefined;
615
633
  onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
634
+ /**
635
+ * Plain click on a time bucket (the plot background — dot/legend clicks
636
+ * and drag-selections never reach this). [bucketStart, bucketEnd) uses
637
+ * the same adjacent-row width derivation as drag-to-select. Fires only
638
+ * when no toggle-selection is active: a click that clears an active
639
+ * legend/dot selection keeps its existing meaning.
640
+ */
641
+ onBucketClick?:
642
+ | ((
643
+ bucketStart: Date,
644
+ bucketEnd: Date,
645
+ // The clicked row: one numeric entry per series + the axis keys.
646
+ valuesAtBucket: Record<string, number | string>,
647
+ ) => void)
648
+ | undefined;
649
+ /**
650
+ * Categories rendered as compare-to-previous-period ghosts: dashed,
651
+ * faded, no fill, no dots — context, not data.
652
+ */
653
+ ghostCategories?: Array<string> | undefined;
616
654
  /**
617
655
  * Render a shaded "expected range" band underneath the data lines.
618
656
  * Both keys must already be present on every entry of `data` (the
@@ -661,6 +699,8 @@ const AreaChart: React.ForwardRefExoticComponent<
661
699
  formattedTimeReferenceLines,
662
700
  formattedReferenceRegions,
663
701
  onTimeRangeSelect,
702
+ onBucketClick,
703
+ ghostCategories,
664
704
  ...other
665
705
  } = props;
666
706
  const CustomTooltip: React.ComponentType<TooltipProps> | undefined =
@@ -928,7 +968,12 @@ const AreaChart: React.ForwardRefExoticComponent<
928
968
  <ResponsiveContainer>
929
969
  <RechartsAreaChart
930
970
  data={data}
931
- syncId={props.syncid?.toString() || ""}
971
+ /*
972
+ * Omitted, not "": recharts treats "" as a real sync
973
+ * channel, so every unsynced chart page-wide would
974
+ * accidentally sync with every other one.
975
+ */
976
+ {...(props.syncid ? { syncId: props.syncid.toString() } : {})}
932
977
  {...(hasOnTimeRangeSelect
933
978
  ? {
934
979
  onMouseDown: handleRangeSelectMouseDown,
@@ -936,19 +981,51 @@ const AreaChart: React.ForwardRefExoticComponent<
936
981
  onMouseUp: handleRangeSelectMouseUp,
937
982
  }
938
983
  : {})}
939
- onClick={
940
- hasOnValueChange && (activeLegend || activeDot)
941
- ? () => {
942
- // Ignore the click that follows a drag-to-select.
943
- if (suppressNextClickRef.current) {
944
- return;
945
- }
946
- setActiveDot(undefined);
947
- setActiveLegend(undefined);
948
- onValueChange?.(null);
949
- }
950
- : () => {}
951
- }
984
+ onClick={(chartState: RangeSelectionChartState) => {
985
+ // Ignore the click that follows a drag-to-select.
986
+ if (suppressNextClickRef.current) {
987
+ return;
988
+ }
989
+ /*
990
+ * A click while a legend/dot selection is active keeps its
991
+ * long-standing meaning — clear the selection — and never
992
+ * also pins a bucket.
993
+ */
994
+ if (hasOnValueChange && (activeLegend || activeDot)) {
995
+ setActiveDot(undefined);
996
+ setActiveLegend(undefined);
997
+ onValueChange?.(null);
998
+ return;
999
+ }
1000
+ if (!onBucketClick) {
1001
+ return;
1002
+ }
1003
+ const rowIndex: number | null =
1004
+ getRowIndexFromChartState(chartState);
1005
+ if (rowIndex === null) {
1006
+ return;
1007
+ }
1008
+ const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
1009
+ if (!bucketStart) {
1010
+ return;
1011
+ }
1012
+ /*
1013
+ * Cover the full bucket: width from adjacent row dates, the
1014
+ * same derivation drag-to-select uses.
1015
+ */
1016
+ const adjacentDate: Date | null =
1017
+ rowIndex > 0
1018
+ ? getBucketDateAtIndex(rowIndex - 1)
1019
+ : getBucketDateAtIndex(rowIndex + 1);
1020
+ const bucketWidthInMs: number = adjacentDate
1021
+ ? Math.abs(bucketStart.getTime() - adjacentDate.getTime())
1022
+ : 0;
1023
+ onBucketClick(
1024
+ bucketStart,
1025
+ new Date(bucketStart.getTime() + bucketWidthInMs),
1026
+ (data[rowIndex] || {}) as Record<string, number | string>,
1027
+ );
1028
+ }}
952
1029
  margin={{
953
1030
  bottom: (xAxisLabel
954
1031
  ? 40
@@ -1173,6 +1250,8 @@ const AreaChart: React.ForwardRefExoticComponent<
1173
1250
  ) as ChartColorValue;
1174
1251
  const hex: string = getColorHex(colorKey);
1175
1252
  const isCustomColor: boolean = isHexColorValue(colorKey);
1253
+ const isGhost: boolean =
1254
+ ghostCategories?.includes(category) || false;
1176
1255
 
1177
1256
  return (
1178
1257
  <Area
@@ -1181,92 +1260,103 @@ const AreaChart: React.ForwardRefExoticComponent<
1181
1260
  type={props.curve || ChartCurve.MONOTONE}
1182
1261
  dataKey={category}
1183
1262
  stroke={hex}
1184
- strokeWidth={2}
1263
+ strokeWidth={isGhost ? 1.5 : 2}
1185
1264
  strokeLinejoin="round"
1186
1265
  strokeLinecap="round"
1187
1266
  fill={`url(#${gradientId})`}
1188
- fillOpacity={1}
1267
+ fillOpacity={isGhost ? 0 : 1}
1189
1268
  strokeOpacity={
1190
- activeDot || (activeLegend && activeLegend !== category)
1269
+ (activeDot || (activeLegend && activeLegend !== category)
1191
1270
  ? 0.3
1192
- : 1
1271
+ : 1) * (isGhost ? 0.45 : 1)
1193
1272
  }
1194
1273
  isAnimationActive={false}
1195
1274
  connectNulls={connectNulls}
1196
1275
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1197
- activeDot={(dotProps: any) => {
1198
- const {
1199
- cx: cxCoord,
1200
- cy: cyCoord,
1201
- stroke,
1202
- strokeLinecap: slc,
1203
- strokeLinejoin: slj,
1204
- strokeWidth,
1205
- } = dotProps;
1206
- return (
1207
- <Dot
1208
- className={cx(
1209
- "stroke-white",
1210
- onValueChange ? "cursor-pointer" : "",
1211
- getColorClassName(colorKey, "fill"),
1212
- )}
1213
- cx={cxCoord}
1214
- cy={cyCoord}
1215
- r={5}
1216
- fill={isCustomColor ? hex : ""}
1217
- stroke={stroke}
1218
- strokeLinecap={slc}
1219
- strokeLinejoin={slj}
1220
- strokeWidth={strokeWidth}
1221
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1222
- onClick={(_: any, event: any) => {
1223
- return onDotClick(dotProps, event);
1224
- }}
1225
- />
1226
- );
1227
- }}
1276
+ activeDot={
1277
+ isGhost
1278
+ ? false
1279
+ : (dotProps: any) => {
1280
+ const {
1281
+ cx: cxCoord,
1282
+ cy: cyCoord,
1283
+ stroke,
1284
+ strokeLinecap: slc,
1285
+ strokeLinejoin: slj,
1286
+ strokeWidth,
1287
+ } = dotProps;
1288
+ return (
1289
+ <Dot
1290
+ className={cx(
1291
+ "stroke-white",
1292
+ onValueChange ? "cursor-pointer" : "",
1293
+ getColorClassName(colorKey, "fill"),
1294
+ )}
1295
+ cx={cxCoord}
1296
+ cy={cyCoord}
1297
+ r={5}
1298
+ fill={isCustomColor ? hex : ""}
1299
+ stroke={stroke}
1300
+ strokeLinecap={slc}
1301
+ strokeLinejoin={slj}
1302
+ strokeWidth={strokeWidth}
1303
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1304
+ onClick={(_: any, event: any) => {
1305
+ return onDotClick(dotProps, event);
1306
+ }}
1307
+ />
1308
+ );
1309
+ }
1310
+ }
1228
1311
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1229
- dot={(dotProps: any) => {
1230
- const {
1231
- stroke,
1232
- strokeLinecap: slc,
1233
- strokeLinejoin: slj,
1234
- strokeWidth,
1235
- cx: cxCoord,
1236
- cy: cyCoord,
1237
- index: dotIndex,
1238
- } = dotProps;
1239
-
1240
- if (
1241
- (hasOnlyOneValueForKey(data, category) &&
1242
- !(
1243
- activeDot ||
1244
- (activeLegend && activeLegend !== category)
1245
- )) ||
1246
- (activeDot?.index === dotIndex &&
1247
- activeDot?.dataKey === category)
1248
- ) {
1249
- return (
1250
- <Dot
1251
- key={dotIndex}
1252
- cx={cxCoord}
1253
- cy={cyCoord}
1254
- r={5}
1255
- stroke={stroke}
1256
- fill={isCustomColor ? hex : ""}
1257
- strokeLinecap={slc}
1258
- strokeLinejoin={slj}
1259
- strokeWidth={strokeWidth}
1260
- className={cx(
1261
- "stroke-white",
1262
- onValueChange ? "cursor-pointer" : "",
1263
- getColorClassName(colorKey, "fill"),
1264
- )}
1265
- />
1266
- );
1267
- }
1268
- return <React.Fragment key={dotIndex}></React.Fragment>;
1269
- }}
1312
+ dot={
1313
+ isGhost
1314
+ ? false
1315
+ : (dotProps: any) => {
1316
+ const {
1317
+ stroke,
1318
+ strokeLinecap: slc,
1319
+ strokeLinejoin: slj,
1320
+ strokeWidth,
1321
+ cx: cxCoord,
1322
+ cy: cyCoord,
1323
+ index: dotIndex,
1324
+ } = dotProps;
1325
+
1326
+ if (
1327
+ (hasOnlyOneValueForKey(data, category) &&
1328
+ !(
1329
+ activeDot ||
1330
+ (activeLegend && activeLegend !== category)
1331
+ )) ||
1332
+ (activeDot?.index === dotIndex &&
1333
+ activeDot?.dataKey === category)
1334
+ ) {
1335
+ return (
1336
+ <Dot
1337
+ key={dotIndex}
1338
+ cx={cxCoord}
1339
+ cy={cyCoord}
1340
+ r={5}
1341
+ stroke={stroke}
1342
+ fill={isCustomColor ? hex : ""}
1343
+ strokeLinecap={slc}
1344
+ strokeLinejoin={slj}
1345
+ strokeWidth={strokeWidth}
1346
+ className={cx(
1347
+ "stroke-white",
1348
+ onValueChange ? "cursor-pointer" : "",
1349
+ getColorClassName(colorKey, "fill"),
1350
+ )}
1351
+ />
1352
+ );
1353
+ }
1354
+ return (
1355
+ <React.Fragment key={dotIndex}></React.Fragment>
1356
+ );
1357
+ }
1358
+ }
1359
+ {...(isGhost ? { strokeDasharray: "6 4" } : {})}
1270
1360
  />
1271
1361
  );
1272
1362
  })}
@@ -1310,7 +1400,9 @@ const AreaChart: React.ForwardRefExoticComponent<
1310
1400
  {...(region.original.onClick
1311
1401
  ? {
1312
1402
  style: { cursor: "pointer" as const },
1313
- onClick: (): void => {
1403
+ onClick: (...args: Array<unknown>): void => {
1404
+ // Never let an annotation click also pin a bucket.
1405
+ stopChartEventPropagation(...args);
1314
1406
  if (suppressNextClickRef.current) {
1315
1407
  return;
1316
1408
  }
@@ -1352,7 +1444,9 @@ const AreaChart: React.ForwardRefExoticComponent<
1352
1444
  {...(timeRefLine.original.onClick
1353
1445
  ? {
1354
1446
  style: { cursor: "pointer" as const },
1355
- onClick: (): void => {
1447
+ onClick: (...args: Array<unknown>): void => {
1448
+ // Never let an annotation click also pin a bucket.
1449
+ stopChartEventPropagation(...args);
1356
1450
  if (suppressNextClickRef.current) {
1357
1451
  return;
1358
1452
  }
@@ -1398,7 +1492,9 @@ const AreaChart: React.ForwardRefExoticComponent<
1398
1492
  stroke="var(--ou-chart-marker-ring, #ffffff)"
1399
1493
  strokeWidth={2}
1400
1494
  style={{ cursor: "pointer" }}
1401
- onClick={() => {
1495
+ onClick={(...args: Array<unknown>) => {
1496
+ // Never let an exemplar click also pin a bucket.
1497
+ stopChartEventPropagation(...args);
1402
1498
  // Ignore the click that follows a drag-to-select.
1403
1499
  if (suppressNextClickRef.current) {
1404
1500
  return;
@@ -843,7 +843,12 @@ const BarChart: React.ForwardRefExoticComponent<
843
843
  <ResponsiveContainer>
844
844
  <RechartsBarChart
845
845
  data={data}
846
- syncId={props.syncid?.toString() || ""}
846
+ /*
847
+ * Omitted, not "": recharts treats "" as a real sync
848
+ * channel, so every unsynced chart page-wide would
849
+ * accidentally sync with every other one.
850
+ */
851
+ {...(props.syncid ? { syncId: props.syncid.toString() } : {})}
847
852
  {...(hasOnValueChange && (activeLegend || activeBar)
848
853
  ? {
849
854
  onClick: handleChartClick,