@oneuptime/common 12.0.19 → 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 (124) hide show
  1. package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
  2. package/Models/AnalyticsModels/Index.ts +2 -0
  3. package/Server/API/EnterpriseLicenseAPI.ts +94 -19
  4. package/Server/EnvironmentConfig.ts +15 -5
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
  7. package/Server/Services/ChangeEventService.ts +11 -0
  8. package/Server/Services/EnterpriseLicenseService.ts +3 -3
  9. package/Server/Services/Index.ts +2 -0
  10. package/Server/Services/ProjectService.ts +104 -0
  11. package/Server/Utils/Attribution.ts +11 -11
  12. package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
  13. package/Server/Utils/Response.ts +2 -2
  14. package/Server/Utils/StartServer.ts +2 -2
  15. package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
  16. package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
  17. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
  18. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
  19. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
  20. package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
  21. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
  22. package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
  23. package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
  24. package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
  25. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
  26. package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
  27. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
  28. package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
  29. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
  30. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
  31. package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
  32. package/Tests/Server/Utils/Attribution.test.ts +4 -5
  33. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
  34. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
  35. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
  36. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
  37. package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
  38. package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
  39. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
  40. package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
  41. package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
  42. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
  43. package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
  44. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
  45. package/Types/Analytics/RevenueEvent.ts +1 -0
  46. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
  47. package/Types/Marketing/Attribution.ts +9 -15
  48. package/Types/Marketing/MarketingEvent.ts +19 -12
  49. package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
  50. package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
  51. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
  52. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
  53. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
  54. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
  55. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
  56. package/UI/Components/Charts/Line/LineChart.tsx +23 -1
  57. package/UI/Components/Detail/Detail.tsx +18 -1
  58. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
  59. package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
  60. package/Utils/Telemetry/CrossSignalScope.ts +19 -10
  61. package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
  62. package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
  63. package/build/dist/Models/AnalyticsModels/Index.js +2 -0
  64. package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
  65. package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
  66. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  67. package/build/dist/Server/EnvironmentConfig.js +14 -5
  68. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  73. package/build/dist/Server/Services/ChangeEventService.js +9 -0
  74. package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
  75. package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
  76. package/build/dist/Server/Services/Index.js +2 -0
  77. package/build/dist/Server/Services/Index.js.map +1 -1
  78. package/build/dist/Server/Services/ProjectService.js +88 -0
  79. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  80. package/build/dist/Server/Utils/Attribution.js +6 -6
  81. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
  82. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
  83. package/build/dist/Server/Utils/Response.js +2 -2
  84. package/build/dist/Server/Utils/Response.js.map +1 -1
  85. package/build/dist/Server/Utils/StartServer.js +2 -2
  86. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  87. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
  88. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
  89. package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
  90. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
  91. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
  92. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  93. package/build/dist/Types/Marketing/Attribution.js +9 -15
  94. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  95. package/build/dist/Types/Marketing/MarketingEvent.js +12 -4
  96. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
  97. package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
  98. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  99. package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
  100. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  101. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
  102. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  103. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +114 -51
  104. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  105. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
  106. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  107. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
  108. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  109. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
  110. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
  111. package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
  112. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  113. package/build/dist/UI/Components/Detail/Detail.js +17 -1
  114. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  115. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
  116. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
  117. package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
  118. package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
  119. package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
  120. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  121. package/package.json +1 -1
  122. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
@@ -0,0 +1,347 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import { cleanup, render } from "@testing-library/react";
11
+ import React, { ReactElement } from "react";
12
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
13
+
14
+ const loggerErrorMock: MockFunction = getJestMockFunction();
15
+
16
+ /*
17
+ * The arrow wrapper is load bearing: jest.mock is hoisted above the compiled
18
+ * requires, so loggerErrorMock is still unassigned when the factory runs.
19
+ * Dereferencing it lazily, at call time, is what makes this work.
20
+ */
21
+ jest.mock("../../../../UI/Utils/Logger", () => {
22
+ return {
23
+ __esModule: true,
24
+ Logger: {
25
+ error: (...args: Array<any>) => {
26
+ return loggerErrorMock(...args);
27
+ },
28
+ warn: () => {
29
+ return undefined;
30
+ },
31
+ log: () => {
32
+ return undefined;
33
+ },
34
+ info: () => {
35
+ return undefined;
36
+ },
37
+ },
38
+ };
39
+ });
40
+
41
+ import Detail from "../../../../UI/Components/Detail/Detail";
42
+ import Field from "../../../../UI/Components/Detail/Field";
43
+ import FieldType from "../../../../UI/Components/Types/FieldType";
44
+ import IncidentSeverity from "../../../../Models/DatabaseModels/IncidentSeverity";
45
+ import BadDataException from "../../../../Types/Exception/BadDataException";
46
+ import Color from "../../../../Types/Color";
47
+ import ObjectID from "../../../../Types/ObjectID";
48
+
49
+ /*
50
+ * Detail's getField is a plain closure called from Detail's own render, so a
51
+ * throw out of a field's getElement used to unwind the entire route up to the
52
+ * app level ErrorBoundary - replacing the whole page with "Something went
53
+ * wrong" over one bad cell. That is issue #3374: an Incident Episode with no
54
+ * severity threw "Episode Severity not found" from the severity renderer and
55
+ * took the detail page with it.
56
+ *
57
+ * The two call sites in that issue are fixed at source, but the same shape
58
+ * exists in ~20 other getElement callbacks across the dashboard. These tests
59
+ * pin the framework level guard: a throwing field renderer degrades to
60
+ * whatever the field type already rendered, logs the misconfiguration, and
61
+ * costs exactly one field - never the page.
62
+ */
63
+
64
+ interface DetailItem {
65
+ incidentSeverity?: IncidentSeverity | undefined;
66
+ title?: string | undefined;
67
+ description?: string | undefined;
68
+ }
69
+
70
+ type RenderDetailFunction = (
71
+ item: DetailItem,
72
+ fields: Array<Field<DetailItem>>,
73
+ ) => HTMLElement;
74
+
75
+ const renderDetail: RenderDetailFunction = (
76
+ item: DetailItem,
77
+ fields: Array<Field<DetailItem>>,
78
+ ): HTMLElement => {
79
+ const { container } = render(
80
+ <Detail<DetailItem>
81
+ item={item}
82
+ fields={fields}
83
+ showDetailsInNumberOfColumns={1}
84
+ />,
85
+ );
86
+
87
+ return container;
88
+ };
89
+
90
+ type BuildSeverityFunction = (name: string, color?: Color) => IncidentSeverity;
91
+
92
+ const buildSeverity: BuildSeverityFunction = (
93
+ name: string,
94
+ color?: Color,
95
+ ): IncidentSeverity => {
96
+ const severity: IncidentSeverity = new IncidentSeverity();
97
+ severity._id = new ObjectID("severity-id").toString();
98
+ severity.name = name;
99
+
100
+ if (color) {
101
+ severity.color = color;
102
+ }
103
+
104
+ return severity;
105
+ };
106
+
107
+ type ThrowingFieldFunction = (
108
+ key: keyof DetailItem,
109
+ title: string,
110
+ error: unknown,
111
+ ) => Field<DetailItem>;
112
+
113
+ const throwingField: ThrowingFieldFunction = (
114
+ key: keyof DetailItem,
115
+ title: string,
116
+ error: unknown,
117
+ ): Field<DetailItem> => {
118
+ return {
119
+ key: key,
120
+ title: title,
121
+ fieldType: FieldType.Element,
122
+ getElement: (): ReactElement => {
123
+ throw error;
124
+ },
125
+ };
126
+ };
127
+
128
+ const badgeSelector: string = '[data-dropdown-value-badge="true"]';
129
+
130
+ beforeEach(() => {
131
+ loggerErrorMock.mockClear();
132
+ });
133
+
134
+ afterEach(() => {
135
+ cleanup();
136
+ });
137
+
138
+ describe("Detail: a throwing field renderer never takes down the page", () => {
139
+ test("does not rethrow out of render when getElement throws", () => {
140
+ expect(() => {
141
+ renderDetail({ title: "Episode one" }, [
142
+ throwingField("title", "Episode Title", new Error("boom")),
143
+ ]);
144
+ }).not.toThrow();
145
+ });
146
+
147
+ test("renders the surrounding grid, so the route survives", () => {
148
+ const container: HTMLElement = renderDetail({ title: "Episode one" }, [
149
+ throwingField("title", "Episode Title", new Error("boom")),
150
+ ]);
151
+
152
+ // The label is still rendered - the row exists, only its value is missing.
153
+ expect(container.textContent).toContain("Episode Title");
154
+ });
155
+
156
+ test("costs exactly one field - the siblings still render their values", () => {
157
+ const container: HTMLElement = renderDetail(
158
+ { title: "Episode one", description: "A long running episode" },
159
+ [
160
+ throwingField("title", "Episode Title", new Error("boom")),
161
+ {
162
+ key: "description",
163
+ title: "Description",
164
+ fieldType: FieldType.Text,
165
+ },
166
+ ],
167
+ );
168
+
169
+ expect(container.textContent).toContain("Description");
170
+ expect(container.textContent).toContain("A long running episode");
171
+ });
172
+
173
+ test("logs the misconfiguration once, naming the field and the message", () => {
174
+ renderDetail({ title: "Episode one" }, [
175
+ throwingField(
176
+ "title",
177
+ "Episode Title",
178
+ new BadDataException("Episode Severity not found"),
179
+ ),
180
+ ]);
181
+
182
+ expect(loggerErrorMock).toHaveBeenCalledTimes(1);
183
+
184
+ const logged: string = String(loggerErrorMock.mock.calls[0]?.[0]);
185
+
186
+ expect(logged).toContain("title");
187
+ expect(logged).toContain("Episode Severity not found");
188
+ });
189
+
190
+ test("logs once per throwing field when several fields throw", () => {
191
+ const container: HTMLElement = renderDetail(
192
+ { title: "Episode one", description: "A long running episode" },
193
+ [
194
+ throwingField("title", "Episode Title", new Error("first")),
195
+ throwingField("description", "Description", new Error("second")),
196
+ ],
197
+ );
198
+
199
+ expect(loggerErrorMock).toHaveBeenCalledTimes(2);
200
+ expect(container.textContent).toContain("Episode Title");
201
+ expect(container.textContent).toContain("Description");
202
+ });
203
+
204
+ test("catches a thrown non Error value and still logs it", () => {
205
+ expect(() => {
206
+ renderDetail({ title: "Episode one" }, [
207
+ throwingField("title", "Episode Title", "a bare string"),
208
+ ]);
209
+ }).not.toThrow();
210
+
211
+ expect(loggerErrorMock).toHaveBeenCalledTimes(1);
212
+ expect(String(loggerErrorMock.mock.calls[0]?.[0])).toContain(
213
+ "a bare string",
214
+ );
215
+ });
216
+
217
+ test("keeps the entity badge the field type already rendered", () => {
218
+ /*
219
+ * data is computed from the field key and transformed by the Entity
220
+ * branch before getElement runs, so swallowing the throw leaves the
221
+ * framework's own rendering of the relation in place.
222
+ */
223
+ const container: HTMLElement = renderDetail(
224
+ { incidentSeverity: buildSeverity("Critical", new Color("#ef4444")) },
225
+ [
226
+ {
227
+ key: "incidentSeverity",
228
+ title: "Episode Severity",
229
+ fieldType: FieldType.Entity,
230
+ getElement: (): ReactElement => {
231
+ throw new BadDataException("Episode Severity not found");
232
+ },
233
+ },
234
+ ],
235
+ );
236
+
237
+ const badge: HTMLElement | null =
238
+ container.querySelector<HTMLElement>(badgeSelector);
239
+
240
+ expect(badge).not.toBeNull();
241
+ expect(badge?.textContent).toContain("Critical");
242
+ });
243
+
244
+ test("falls back to the field placeholder when the relation is absent", () => {
245
+ const container: HTMLElement = renderDetail({}, [
246
+ {
247
+ key: "incidentSeverity",
248
+ title: "Episode Severity",
249
+ fieldType: FieldType.Entity,
250
+ placeholder: "No severity",
251
+ getElement: (): ReactElement => {
252
+ throw new BadDataException("Episode Severity not found");
253
+ },
254
+ },
255
+ ]);
256
+
257
+ expect(container.textContent).toContain("No severity");
258
+ expect(container.querySelector(badgeSelector)).toBeNull();
259
+ });
260
+
261
+ test("never shows the app level error boundary copy", () => {
262
+ const container: HTMLElement = renderDetail({ title: "Episode one" }, [
263
+ throwingField("title", "Episode Title", new Error("boom")),
264
+ ]);
265
+
266
+ expect(container.textContent).not.toContain("Something went wrong");
267
+ expect(container.textContent).not.toContain(
268
+ "An unexpected error has occurred",
269
+ );
270
+ });
271
+ });
272
+
273
+ describe("Detail: fields that do not throw are untouched by the guard", () => {
274
+ test("renders the element a healthy getElement returns", () => {
275
+ const container: HTMLElement = renderDetail({ title: "Episode one" }, [
276
+ {
277
+ key: "title",
278
+ title: "Episode Title",
279
+ fieldType: FieldType.Element,
280
+ getElement: (item: DetailItem): ReactElement => {
281
+ return <span data-testid="custom">{item.title}</span>;
282
+ },
283
+ },
284
+ ]);
285
+
286
+ expect(
287
+ container.querySelector<HTMLElement>('[data-testid="custom"]')
288
+ ?.textContent,
289
+ ).toEqual("Episode one");
290
+ });
291
+
292
+ test("does not log anything when no field throws", () => {
293
+ renderDetail({ title: "Episode one" }, [
294
+ {
295
+ key: "title",
296
+ title: "Episode Title",
297
+ fieldType: FieldType.Text,
298
+ },
299
+ ]);
300
+
301
+ expect(loggerErrorMock).not.toHaveBeenCalled();
302
+ });
303
+
304
+ test("getElement still wins over the value the field type computed", () => {
305
+ const container: HTMLElement = renderDetail(
306
+ { incidentSeverity: buildSeverity("Critical", new Color("#ef4444")) },
307
+ [
308
+ {
309
+ key: "incidentSeverity",
310
+ title: "Episode Severity",
311
+ fieldType: FieldType.Entity,
312
+ getElement: (): ReactElement => {
313
+ return <span data-testid="custom">Handled</span>;
314
+ },
315
+ },
316
+ ],
317
+ );
318
+
319
+ expect(container.querySelector('[data-testid="custom"]')).not.toBeNull();
320
+ expect(container.querySelector(badgeSelector)).toBeNull();
321
+ });
322
+
323
+ test("a getElement that returns a placeholder dash renders the dash", () => {
324
+ /*
325
+ * The shape both episode detail pages now use for an absent severity or
326
+ * state. It must survive the guard untouched and must not log.
327
+ */
328
+ const container: HTMLElement = renderDetail({}, [
329
+ {
330
+ key: "incidentSeverity",
331
+ title: "Episode Severity",
332
+ fieldType: FieldType.Entity,
333
+ getElement: (item: DetailItem): ReactElement => {
334
+ if (!item["incidentSeverity"]) {
335
+ return <>-</>;
336
+ }
337
+
338
+ return <span data-testid="custom">present</span>;
339
+ },
340
+ },
341
+ ]);
342
+
343
+ expect(container.textContent).toContain("Episode Severity");
344
+ expect(container.textContent).toContain("-");
345
+ expect(loggerErrorMock).not.toHaveBeenCalled();
346
+ });
347
+ });