@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
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "@jest/globals";
2
2
  import {
3
3
  DEFAULT_TOOLTIP_MAX_ENTRIES,
4
+ PREVIOUS_PERIOD_SERIES_SUFFIX,
4
5
  PreparedTooltipEntries,
5
6
  SortableTooltipItem,
6
7
  prepareTooltipEntries,
@@ -53,24 +54,24 @@ describe("prepareTooltipEntries", () => {
53
54
  expect(categories(prepared)).toEqual(["cpu1", "cpu2", "cpu10"]);
54
55
  });
55
56
 
56
- test("sorts non-finite values last without dropping them", () => {
57
+ test("sorts non-finite values last and drops band tuples entirely", () => {
57
58
  const prepared: PreparedTooltipEntries<TestItem> = prepareTooltipEntries([
58
59
  item("nan-series", NaN),
59
60
  item("real-low", 1),
60
61
  item("infinite", Infinity),
61
62
  item("real-high", 7),
62
- // The anomaly band's dataKey yields a [low, high] tuple at runtime.
63
+ /*
64
+ * The anomaly band's dataKey yields a [low, high] tuple at runtime
65
+ * — a shaded region, not a series, so it must not render as a row
66
+ * nor count toward the overflow footer.
67
+ */
63
68
  item("band", [2, 9] as unknown as number),
64
69
  ]);
65
70
 
66
71
  expect(categories(prepared).slice(0, 2)).toEqual(["real-high", "real-low"]);
67
72
  // Non-finite tail keeps natural name order among itself.
68
- expect(categories(prepared).slice(2)).toEqual([
69
- "band",
70
- "infinite",
71
- "nan-series",
72
- ]);
73
- expect(prepared.totalCount).toBe(5);
73
+ expect(categories(prepared).slice(2)).toEqual(["infinite", "nan-series"]);
74
+ expect(prepared.totalCount).toBe(4);
74
75
  });
75
76
 
76
77
  test('filters out type "none" entries (hidden click-target lines, band fills)', () => {
@@ -143,6 +144,41 @@ describe("prepareTooltipEntries", () => {
143
144
  }
144
145
  });
145
146
 
147
+ test("previous-period ghosts sort after every live series, whatever their value", () => {
148
+ const prepared: PreparedTooltipEntries<TestItem> = prepareTooltipEntries([
149
+ item(`host-a${PREVIOUS_PERIOD_SERIES_SUFFIX}`, 999),
150
+ item("host-a", 10),
151
+ item("host-b", 50),
152
+ item(`host-b${PREVIOUS_PERIOD_SERIES_SUFFIX}`, 60),
153
+ ]);
154
+
155
+ expect(categories(prepared)).toEqual([
156
+ "host-b",
157
+ "host-a",
158
+ // Ghost tier keeps its own value ordering.
159
+ `host-a${PREVIOUS_PERIOD_SERIES_SUFFIX}`,
160
+ `host-b${PREVIOUS_PERIOD_SERIES_SUFFIX}`,
161
+ ]);
162
+ });
163
+
164
+ test("ghosts never displace live readings under the cap", () => {
165
+ const payload: Array<TestItem> = [];
166
+ for (let index: number = 0; index < DEFAULT_TOOLTIP_MAX_ENTRIES; index++) {
167
+ payload.push(item(`live-${index}`, index));
168
+ payload.push(item(`live-${index}${PREVIOUS_PERIOD_SERIES_SUFFIX}`, 1e6));
169
+ }
170
+
171
+ const prepared: PreparedTooltipEntries<TestItem> =
172
+ prepareTooltipEntries(payload);
173
+
174
+ expect(
175
+ prepared.entries.every((entry: TestItem) => {
176
+ return !entry.category.endsWith(PREVIOUS_PERIOD_SERIES_SUFFIX);
177
+ }),
178
+ ).toBe(true);
179
+ expect(prepared.overflowCount).toBe(DEFAULT_TOOLTIP_MAX_ENTRIES);
180
+ });
181
+
146
182
  test("does not mutate the input payload", () => {
147
183
  const payload: Array<TestItem> = [item("low", 1), item("high", 2)];
148
184
  prepareTooltipEntries(payload);
@@ -597,9 +597,20 @@ describe("toMetricsExplorerQueryParams", () => {
597
597
  });
598
598
 
599
599
  test("reports the signal-specific fields the metric grammar cannot express", () => {
600
+ const result: CrossSignalQueryParams = toMetricsExplorerQueryParams(
601
+ fullScope(),
602
+ "container.cpu.time",
603
+ );
604
+
605
+ /*
606
+ * serviceIds now CARRY: they ride the explorer's one-shot
607
+ * `services` param (resolved to resource.service.name filters on
608
+ * load) instead of being reported dropped.
609
+ */
610
+ expect(result.dropped).toEqual(["traceIds", "spanIds", "severityTexts"]);
600
611
  expect(
601
- toMetricsExplorerQueryParams(fullScope(), "container.cpu.time").dropped,
602
- ).toEqual(["serviceIds", "traceIds", "spanIds", "severityTexts"]);
612
+ JSON.parse(result.params["services"] as string).length,
613
+ ).toBeGreaterThan(0);
603
614
  });
604
615
  });
605
616
 
@@ -7,6 +7,13 @@ enum AnalyticsTableName {
7
7
  * attributes with a keys sidecar for arbitrary-field filtering.
8
8
  */
9
9
  SecurityEvent = "SecurityEventItemV1",
10
+ /*
11
+ * Deploy/config-change markers posted by CI/CD, rendered as dashed
12
+ * vertical lines on metric charts. Peer of LogItemV3 in layout (tenant
13
+ * projectId, per-row retentionDate TTL, attributes Map + keys sidecar)
14
+ * but tiny in volume.
15
+ */
16
+ ChangeEvent = "ChangeEventV1",
10
17
  Metric = "MetricItemV3",
11
18
  ExceptionInstance = "ExceptionItemV3",
12
19
  Span = "SpanItemV3",
@@ -3,19 +3,14 @@
3
3
  * writes it.
4
4
  *
5
5
  * Attribution starts in the browser (Home/Views/head-basic.ejs stores it in
6
- * localStorage) and reaches the server through two unrelated doors:
6
+ * localStorage) and reaches the server through the signup form
7
+ * (App/FeatureSet/Accounts/src/Pages/Register.tsx), which posts it onto the
8
+ * User record.
7
9
  *
8
- * - the signup form (App/FeatureSet/Accounts/src/Pages/Register.tsx), which
9
- * posts it onto the User record;
10
- * - the Cal.com booking webhook (App/API/CalWebhook.ts), which reads it back
11
- * out of the booking metadata the embed carried. Every page that books —
12
- * /enterprise/demo, /support, /enterprise/self-hosted — goes through it.
13
- *
14
- * Both doors are reachable by an unauthenticated caller, so both must whitelist
15
- * and length-bound what they accept. Keeping the key lists here rather than in
16
- * each door means a key added for the browser cannot be silently dropped by one
17
- * of the readers — which is exactly what happened to UTM parameters on the Cal
18
- * path, where the webhook parsed click IDs the embed never sent.
10
+ * That door is reachable by an unauthenticated caller, so it must whitelist and
11
+ * length-bound what it accepts. Keeping the key lists here rather than in the
12
+ * door itself means a key added for the browser cannot be silently dropped on
13
+ * arrival.
19
14
  */
20
15
 
21
16
  // Ad-platform click identifiers, in the spelling each platform uses on the URL.
@@ -31,9 +26,8 @@ export const AdClickIdKeys: Array<string> = [
31
26
  ];
32
27
 
33
28
  /*
34
- * UTM parameters travel as snake_case on a URL and in Cal booking metadata,
35
- * and as camelCase once they are columns on User/Project/MarketingConversion.
36
- * Both spellings are accepted on the way in — Cal booking questions and hand
29
+ * UTM parameters travel as snake_case on a URL and as camelCase once they are
30
+ * columns on User/Project. Both spellings are accepted on the way in — hand
37
31
  * written links produce either — and only the camelCase form is stored.
38
32
  */
39
33
  export const UtmWireKeyToPropertyKey: Record<string, string> = {
@@ -13,7 +13,7 @@ import { JSONObject } from "../JSON";
13
13
  *
14
14
  * - DEDUPLICATION. `eventId` is stable for a given real-world occurrence, so
15
15
  * a redelivery carries the id the first attempt did. Retries are expected
16
- * (the queue retries, and Cal retries into us), so a receiver that keys on
16
+ * (the delivery queue retries), so a receiver that keys on
17
17
  * anything else will double-count.
18
18
  * - ORDERING. Events are delivered per-occurrence with no sequence number.
19
19
  * A signup and a plan change seconds apart may arrive either way round;
@@ -26,7 +26,6 @@ import { JSONObject } from "../JSON";
26
26
  */
27
27
  export enum MarketingEventType {
28
28
  SignUp = "sign_up",
29
- MeetingBooked = "meeting_booked",
30
29
  /*
31
30
  * A project's first paid subscription, created together with the project.
32
31
  *
@@ -45,53 +44,19 @@ export enum MarketingEventType {
45
44
  * whose email is typed in by a human rather than captured from a session —
46
45
  * which is exactly what makes it joinable: set EnterpriseLicense.email to
47
46
  * the address the customer booked with and this event shares an identity
48
- * with the meeting_booked that preceded it, months earlier.
47
+ * with whatever else that address has done, months earlier.
49
48
  */
50
49
  EnterpriseLicenseIssued = "enterprise_license_issued",
51
50
  }
52
51
 
53
- /*
54
- * Which conversation a booked meeting actually is.
55
- *
56
- * All three Cal embeds book the same event type (oneuptimehq/demo), so without
57
- * this the webhook cannot tell a free user's support call from a net-new
58
- * enterprise demo — they arrive as one undifferentiated `meeting_booked`, and
59
- * anything bidding on or reporting against that count values them identically.
60
- *
61
- * The browser event has carried this since the embeds were instrumented; the
62
- * webhook did not, which meant the authoritative record was the one that could
63
- * not make the distinction.
64
- *
65
- * `Unknown` is a real state, not a failure: a booking made through a Cal link
66
- * that was not one of the instrumented embeds genuinely has no kind, and is
67
- * still a booking worth emitting.
68
- */
69
- export enum MeetingBookingKind {
70
- EnterpriseDemo = "enterprise_demo",
71
- SupportCall = "support_call",
72
- ArchitectureAssessment = "architecture_assessment",
73
- Unknown = "unknown",
74
- }
75
-
76
- /*
77
- * The Cal booking-metadata key the embeds write and the webhook reads. Shared
78
- * so a rename cannot land on one side only - the failure mode there is silent,
79
- * because a booking with no recognised kind still succeeds.
80
- */
81
- export const BOOKING_KIND_METADATA_KEY: string = "ou_booking_kind";
82
-
83
- export const MeetingBookingKinds: Array<string> =
84
- Object.values(MeetingBookingKind);
85
-
86
52
  export const MARKETING_EVENT_SCHEMA_VERSION: number = 1;
87
53
 
88
54
  /*
89
55
  * The campaign the converting visitor carried.
90
56
  *
91
- * Copied from the User or Project row for a signup or a plan change, and read
92
- * out of Cal booking metadata for a booked meeting. Every field is optional:
93
- * a conversion with no attribution at all is still a conversion, and is still
94
- * emitted.
57
+ * Copied from the User or Project row the conversion happened on. Every field
58
+ * is optional: a conversion with no attribution at all is still a conversion,
59
+ * and is still emitted.
95
60
  */
96
61
  export interface MarketingEventAttribution extends JSONObject {
97
62
  utmSource?: string | undefined;
@@ -110,17 +75,16 @@ export interface MarketingEvent extends JSONObject {
110
75
  * Stable per real-world occurrence, and the receiver's deduplication key:
111
76
  *
112
77
  * sign_up:{userId}
113
- * meeting_booked:{calBookingUid}
114
78
  * subscription_started:{projectId}
115
79
  * subscription_upgraded:{projectId}:{occurredAt}
116
80
  * subscription_downgraded:{projectId}:{occurredAt}
117
81
  * enterprise_license_issued:{enterpriseLicenseId}
118
82
  *
119
- * Four of these are naturally unique — a user signs up once, a booking has
120
- * one uid, a project has one first subscription, a licence row is issued
121
- * once so Cal retrying a delivery or the queue retrying a job cannot
122
- * produce a second conversion. A plan change can legitimately recur for one
123
- * project, so its id carries the instant it happened.
83
+ * Three of these are naturally unique — a user signs up once, a project has
84
+ * one first subscription, a licence row is issued once — so the queue
85
+ * retrying a job cannot produce a second conversion. A plan change can
86
+ * legitimately recur for one project, so its id carries the instant it
87
+ * happened.
124
88
  */
125
89
  eventId: string;
126
90
  eventType: MarketingEventType;
@@ -63,6 +63,19 @@ export interface ComponentProps {
63
63
  * buckets calls back with the [start, end) of the selected time range.
64
64
  */
65
65
  onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
66
+ // Plain click on a bucket — see ChartLibrary onBucketClick.
67
+ onBucketClick?:
68
+ | ((
69
+ bucketStart: Date,
70
+ bucketEnd: Date,
71
+ valuesAtBucket: Record<string, number | string>,
72
+ ) => void)
73
+ | undefined;
74
+ /*
75
+ * Series rendered as compare-to-previous-period ghosts (dashed, faded,
76
+ * dotless). Names must exist in `data`.
77
+ */
78
+ ghostSeriesNames?: Array<string> | undefined;
66
79
  showLegend?: boolean | undefined;
67
80
  /**
68
81
  * Render a shaded "expected range" band underneath the plotted lines.
@@ -228,6 +241,8 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
228
241
  }
229
242
  onExemplarClick={props.onExemplarClick}
230
243
  onTimeRangeSelect={props.onTimeRangeSelect}
244
+ onBucketClick={props.onBucketClick}
245
+ ghostCategories={props.ghostSeriesNames}
231
246
  anomalyBandLowerKey={bandLower}
232
247
  anomalyBandUpperKey={bandUpper}
233
248
  />
@@ -57,12 +57,27 @@ export interface ComponentProps {
57
57
  charts: Array<Chart>;
58
58
  hideCard?: boolean | undefined;
59
59
  chartCssClass?: string | undefined;
60
+ /**
61
+ * Share one crosshair-sync channel across SEVERAL ChartGroup instances
62
+ * (e.g. every metric widget on one dashboard passes the dashboard id,
63
+ * so hovering an instant on one widget highlights it on all of them).
64
+ * Absent → this group syncs only within itself.
65
+ */
66
+ syncId?: string | undefined;
60
67
  }
61
68
 
62
69
  const ChartGroup: FunctionComponent<ComponentProps> = (
63
70
  props: ComponentProps,
64
71
  ): ReactElement => {
65
- const syncId: string = Text.generateRandomText(10);
72
+ /*
73
+ * Stable per-mount fallback: a plain const regenerated the id on every
74
+ * render, which forced recharts to tear down and re-subscribe its sync
75
+ * listeners on each parent re-render (auto-refresh ticks included).
76
+ */
77
+ const [fallbackSyncId] = useState<string>(() => {
78
+ return Text.generateRandomText(10);
79
+ });
80
+ const syncId: string = props.syncId || fallbackSyncId;
66
81
  const [metricInfoModalChart, setMetricInfoModalChart] =
67
82
  useState<ChartMetricInfo | null>(null);
68
83