@oneuptime/common 12.0.20 → 12.0.22
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.
- package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/UserTwoFactorBackupCode.ts +262 -0
- package/Server/API/UserTwoFactorBackupCodeAPI.ts +258 -0
- package/Server/EnvironmentConfig.ts +1 -8
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.ts +63 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/UserService.ts +42 -0
- package/Server/Services/UserTwoFactorBackupCodeService.ts +355 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Utils/TwoFactorBackupCode.ts +266 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +3 -95
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +64 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
- package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
- package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/UserAuthenticationAPI.test.ts +16 -3
- package/Tests/Server/API/UserTwoFactorAuthAdminAPI.test.ts +18 -2
- package/Tests/Server/API/UserTwoFactorBackupCodeAPI.test.ts +1390 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/UserAuthenticationService.test.ts +23 -1
- package/Tests/Server/Services/UserTwoFactorAuthAdmin.test.ts +21 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeAdminSurface.test.ts +919 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeService.test.ts +862 -0
- package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
- package/Tests/Server/Utils/Attribution.test.ts +4 -5
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
- package/Tests/Server/Utils/TwoFactorBackupCode.test.ts +475 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +44 -8
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +10 -46
- package/Types/UserAuthenticationStatus.ts +16 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +15 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +189 -93
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +6 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +213 -114
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +42 -27
- package/UI/Components/Charts/Line/LineChart.tsx +15 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
- package/Utils/Telemetry/CrossSignalScope.ts +19 -10
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Index.js +2 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js +277 -0
- package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +201 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +1 -7
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ChangeEventService.js +9 -0
- package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +39 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js +327 -0
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js.map +1 -0
- package/build/dist/Server/Utils/Attribution.js +6 -6
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- package/build/dist/Server/Utils/TwoFactorBackupCode.js +269 -0
- package/build/dist/Server/Utils/TwoFactorBackupCode.js.map +1 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Marketing/Attribution.js +9 -15
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +2 -33
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +92 -38
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +8 -8
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +95 -37
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +39 -24
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
|
@@ -0,0 +1,249 @@
|
|
|
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, screen, waitFor } from "@testing-library/react";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* The shared event-overlay hook is what puts "what happened here" on every
|
|
16
|
+
* metric chart: incidents and alerts as solid severity-colored markers
|
|
17
|
+
* with click-through, change events (deploys) as dashed indigo markers.
|
|
18
|
+
* It must fetch nothing on public dashboards and survive any single
|
|
19
|
+
* source failing.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
23
|
+
const analyticsGetListMock: MockFunction = getJestMockFunction();
|
|
24
|
+
const isPublicDashboardMock: MockFunction = getJestMockFunction();
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* The arrow wrappers are load bearing: jest.mock is hoisted above the
|
|
28
|
+
* compiled requires, so the mock variables above are still unassigned when
|
|
29
|
+
* the factory runs.
|
|
30
|
+
*/
|
|
31
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
32
|
+
return {
|
|
33
|
+
__esModule: true,
|
|
34
|
+
default: {
|
|
35
|
+
getList: (...args: Array<any>) => {
|
|
36
|
+
return getListMock(...args);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
jest.mock("../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI", () => {
|
|
43
|
+
return {
|
|
44
|
+
__esModule: true,
|
|
45
|
+
default: {
|
|
46
|
+
getList: (...args: Array<any>) => {
|
|
47
|
+
return analyticsGetListMock(...args);
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
jest.mock(
|
|
54
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Dashboard/Utils/PublicDashboardContext",
|
|
55
|
+
() => {
|
|
56
|
+
return {
|
|
57
|
+
__esModule: true,
|
|
58
|
+
isPublicDashboard: (...args: Array<any>) => {
|
|
59
|
+
return isPublicDashboardMock(...args);
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
import useEventTimeReferenceLines, {
|
|
66
|
+
EventTimeReferenceLines,
|
|
67
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/UseEventTimeReferenceLines";
|
|
68
|
+
import ChartTimeReferenceLineProps from "../../../UI/Components/Charts/Types/TimeReferenceLineProps";
|
|
69
|
+
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
70
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
71
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
72
|
+
|
|
73
|
+
const WINDOW: InBetween<Date> = new InBetween<Date>(
|
|
74
|
+
new Date("2026-08-20T10:00:00.000Z"),
|
|
75
|
+
new Date("2026-08-20T11:00:00.000Z"),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
79
|
+
"9e1b6b0e-0000-4000-8000-000000000011",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
function Probe(props: {
|
|
83
|
+
enabled: boolean;
|
|
84
|
+
window: InBetween<Date> | null;
|
|
85
|
+
}): React.ReactElement {
|
|
86
|
+
const { lines, markerCount }: EventTimeReferenceLines =
|
|
87
|
+
useEventTimeReferenceLines({
|
|
88
|
+
enabled: props.enabled,
|
|
89
|
+
window: props.window,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div>
|
|
94
|
+
<span data-testid="marker-count">{markerCount}</span>
|
|
95
|
+
{lines.map((line: ChartTimeReferenceLineProps, index: number) => {
|
|
96
|
+
return (
|
|
97
|
+
<div
|
|
98
|
+
key={index}
|
|
99
|
+
data-testid="marker"
|
|
100
|
+
data-color={line.color}
|
|
101
|
+
data-dash={line.strokeDasharray || "solid"}
|
|
102
|
+
data-clickable={line.onClick ? "yes" : "no"}
|
|
103
|
+
>
|
|
104
|
+
{line.label}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
getListMock.mockReset();
|
|
114
|
+
analyticsGetListMock.mockReset();
|
|
115
|
+
isPublicDashboardMock.mockReset();
|
|
116
|
+
isPublicDashboardMock.mockReturnValue(false);
|
|
117
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
jest.restoreAllMocks();
|
|
122
|
+
cleanup();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("useEventTimeReferenceLines", () => {
|
|
126
|
+
test("maps incidents, alerts, and change events onto distinct marker styles", async () => {
|
|
127
|
+
getListMock.mockImplementation((args: { modelType: { name: string } }) => {
|
|
128
|
+
if (args.modelType.name === "Incident") {
|
|
129
|
+
return Promise.resolve({
|
|
130
|
+
data: [
|
|
131
|
+
{
|
|
132
|
+
id: new ObjectID("11111111-0000-4000-8000-000000000001"),
|
|
133
|
+
createdAt: "2026-08-20T10:10:00.000Z",
|
|
134
|
+
title: "API is down",
|
|
135
|
+
incidentSeverity: { color: "#123456" },
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return Promise.resolve({
|
|
141
|
+
data: [
|
|
142
|
+
{
|
|
143
|
+
id: new ObjectID("22222222-0000-4000-8000-000000000002"),
|
|
144
|
+
createdAt: "2026-08-20T10:20:00.000Z",
|
|
145
|
+
title: "High latency",
|
|
146
|
+
alertSeverity: undefined,
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
analyticsGetListMock.mockResolvedValue({
|
|
152
|
+
data: [
|
|
153
|
+
{
|
|
154
|
+
time: "2026-08-20T10:03:00.000Z",
|
|
155
|
+
title: "v2.31.0",
|
|
156
|
+
eventType: "deployment",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
render(<Probe enabled={true} window={WINDOW} />);
|
|
162
|
+
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
expect(screen.getByTestId("marker-count").textContent).toBe("3");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const markers: Array<HTMLElement> = screen.getAllByTestId("marker");
|
|
168
|
+
const byLabel: (needle: string) => HTMLElement = (
|
|
169
|
+
needle: string,
|
|
170
|
+
): HTMLElement => {
|
|
171
|
+
const found: HTMLElement | undefined = markers.find(
|
|
172
|
+
(marker: HTMLElement) => {
|
|
173
|
+
return (marker.textContent || "").includes(needle);
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
if (!found) {
|
|
177
|
+
throw new Error(`no marker containing ${needle}`);
|
|
178
|
+
}
|
|
179
|
+
return found;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const incident: HTMLElement = byLabel("Incident: API is down");
|
|
183
|
+
expect(incident.dataset["color"]).toBe("#123456");
|
|
184
|
+
expect(incident.dataset["dash"]).toBe("solid");
|
|
185
|
+
expect(incident.dataset["clickable"]).toBe("yes");
|
|
186
|
+
|
|
187
|
+
// Alert without a severity color falls back to amber.
|
|
188
|
+
expect(byLabel("Alert: High latency").dataset["color"]).toBe("#fbbf24");
|
|
189
|
+
|
|
190
|
+
const deploy: HTMLElement = byLabel("Deploy: v2.31.0");
|
|
191
|
+
expect(deploy.dataset["color"]).toBe("#6366f1");
|
|
192
|
+
expect(deploy.dataset["dash"]).toBe("4 4");
|
|
193
|
+
// Change events have no detail page — no dead-end click handler.
|
|
194
|
+
expect(deploy.dataset["clickable"]).toBe("no");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("one source failing never takes the others down", async () => {
|
|
198
|
+
getListMock.mockImplementation((args: { modelType: { name: string } }) => {
|
|
199
|
+
if (args.modelType.name === "Incident") {
|
|
200
|
+
return Promise.resolve({
|
|
201
|
+
data: [
|
|
202
|
+
{
|
|
203
|
+
id: new ObjectID("11111111-0000-4000-8000-000000000001"),
|
|
204
|
+
createdAt: "2026-08-20T10:10:00.000Z",
|
|
205
|
+
title: "Still here",
|
|
206
|
+
incidentSeverity: undefined,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return Promise.resolve({ data: [] });
|
|
212
|
+
});
|
|
213
|
+
analyticsGetListMock.mockRejectedValue(new Error("clickhouse hiccup"));
|
|
214
|
+
|
|
215
|
+
render(<Probe enabled={true} window={WINDOW} />);
|
|
216
|
+
|
|
217
|
+
await waitFor(() => {
|
|
218
|
+
expect(screen.getByTestId("marker-count").textContent).toBe("1");
|
|
219
|
+
});
|
|
220
|
+
expect(screen.getByText(/Incident: Still here/)).toBeInTheDocument();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("public dashboards fetch nothing at all", async () => {
|
|
224
|
+
isPublicDashboardMock.mockReturnValue(true);
|
|
225
|
+
|
|
226
|
+
render(<Probe enabled={true} window={WINDOW} />);
|
|
227
|
+
|
|
228
|
+
// Give any (wrong) fetch a chance to fire.
|
|
229
|
+
await new Promise((resolve: (value: unknown) => void) => {
|
|
230
|
+
setTimeout(resolve, 20);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(getListMock).not.toHaveBeenCalled();
|
|
234
|
+
expect(analyticsGetListMock).not.toHaveBeenCalled();
|
|
235
|
+
expect(screen.getByTestId("marker-count").textContent).toBe("0");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("disabled and windowless states fetch nothing and yield no lines", async () => {
|
|
239
|
+
render(<Probe enabled={false} window={WINDOW} />);
|
|
240
|
+
render(<Probe enabled={true} window={null} />);
|
|
241
|
+
|
|
242
|
+
await new Promise((resolve: (value: unknown) => void) => {
|
|
243
|
+
setTimeout(resolve, 20);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(getListMock).not.toHaveBeenCalled();
|
|
247
|
+
expect(analyticsGetListMock).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1,315 @@
|
|
|
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 {
|
|
11
|
+
cleanup,
|
|
12
|
+
fireEvent,
|
|
13
|
+
render,
|
|
14
|
+
screen,
|
|
15
|
+
waitFor,
|
|
16
|
+
} from "@testing-library/react";
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* PR C surfaces on the investigation drawer: the deterministic findings
|
|
22
|
+
* card (built from the drawer's OWN evidence — log signal, patterns,
|
|
23
|
+
* event markers), the "Explain with AI" hand-off (prefill, never
|
|
24
|
+
* auto-send), and "Save to incident" (the evidence pinned to an incident
|
|
25
|
+
* timeline as a markdown internal note).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const histogramMock: MockFunction = getJestMockFunction();
|
|
29
|
+
const patternsMock: MockFunction = getJestMockFunction();
|
|
30
|
+
const eventLinesMock: MockFunction = getJestMockFunction();
|
|
31
|
+
|
|
32
|
+
jest.mock(
|
|
33
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Logs/LogsInsightsApi",
|
|
34
|
+
() => {
|
|
35
|
+
return {
|
|
36
|
+
__esModule: true,
|
|
37
|
+
fetchLogsHistogramRaw: (...args: Array<any>) => {
|
|
38
|
+
return histogramMock(...args);
|
|
39
|
+
},
|
|
40
|
+
fetchTopErrorPatterns: (...args: Array<any>) => {
|
|
41
|
+
return patternsMock(...args);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// The marker hook is separately tested — here it hands back a fixed set.
|
|
48
|
+
jest.mock(
|
|
49
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/UseEventTimeReferenceLines",
|
|
50
|
+
() => {
|
|
51
|
+
return {
|
|
52
|
+
__esModule: true,
|
|
53
|
+
default: (...args: Array<any>) => {
|
|
54
|
+
return eventLinesMock(...args);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
jest.mock(
|
|
61
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Telemetry/TelemetryCompanionSignalTabs",
|
|
62
|
+
() => {
|
|
63
|
+
return {
|
|
64
|
+
__esModule: true,
|
|
65
|
+
default: (): React.ReactElement => {
|
|
66
|
+
return React.createElement("div", { "data-testid": "companion-tabs" });
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
jest.mock(
|
|
73
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Metrics/EmbeddedMetricCard",
|
|
74
|
+
() => {
|
|
75
|
+
return {
|
|
76
|
+
__esModule: true,
|
|
77
|
+
default: (): React.ReactElement => {
|
|
78
|
+
return React.createElement("div", {
|
|
79
|
+
"data-testid": "embedded-metric-card",
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
import InvestigationDrawer from "../../../../App/FeatureSet/Dashboard/src/Components/Telemetry/InvestigationDrawer";
|
|
87
|
+
import EventName from "../../../../App/FeatureSet/Dashboard/src/Utils/EventName";
|
|
88
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
89
|
+
import IncidentInternalNote from "../../../Models/DatabaseModels/IncidentInternalNote";
|
|
90
|
+
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
91
|
+
import MetricsAggregationType from "../../../Types/Metrics/MetricsAggregationType";
|
|
92
|
+
import MetricViewData from "../../../Types/Metrics/MetricViewData";
|
|
93
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
94
|
+
import GlobalEvents from "../../../UI/Utils/GlobalEvents";
|
|
95
|
+
import ModelAPI from "../../../UI/Utils/ModelAPI/ModelAPI";
|
|
96
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
97
|
+
|
|
98
|
+
const WINDOW: InBetween<Date> = new InBetween<Date>(
|
|
99
|
+
new Date("2026-08-20T10:00:00.000Z"),
|
|
100
|
+
new Date("2026-08-20T10:15:00.000Z"),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
104
|
+
"11111111-1111-1111-1111-111111111111",
|
|
105
|
+
);
|
|
106
|
+
const INCIDENT_ID: ObjectID = new ObjectID(
|
|
107
|
+
"22222222-2222-2222-2222-222222222222",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
function buildViewData(): MetricViewData {
|
|
111
|
+
return {
|
|
112
|
+
queryConfigs: [
|
|
113
|
+
{
|
|
114
|
+
metricAliasData: { metricVariable: "a" },
|
|
115
|
+
metricQueryData: {
|
|
116
|
+
filterData: {
|
|
117
|
+
metricName: "cpu.usage",
|
|
118
|
+
attributes: { "host.name": "web-01" },
|
|
119
|
+
aggegationType: MetricsAggregationType.Avg,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
formulaConfigs: [],
|
|
125
|
+
startAndEndDate: null,
|
|
126
|
+
} as unknown as MetricViewData;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildIncident(): Incident {
|
|
130
|
+
return {
|
|
131
|
+
id: INCIDENT_ID,
|
|
132
|
+
_id: INCIDENT_ID.toString(),
|
|
133
|
+
title: "API latency spike",
|
|
134
|
+
incidentNumberWithPrefix: "INC-42",
|
|
135
|
+
incidentNumber: 42,
|
|
136
|
+
} as unknown as Incident;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
let dispatchSpy: ReturnType<typeof jest.spyOn>;
|
|
140
|
+
let getListSpy: ReturnType<typeof jest.spyOn>;
|
|
141
|
+
let createSpy: ReturnType<typeof jest.spyOn>;
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
histogramMock.mockReset();
|
|
145
|
+
patternsMock.mockReset();
|
|
146
|
+
eventLinesMock.mockReset();
|
|
147
|
+
histogramMock.mockReturnValue(
|
|
148
|
+
Promise.resolve([
|
|
149
|
+
{ time: "2026-08-20T10:00:00.000Z", severity: "Information", count: 90 },
|
|
150
|
+
{ time: "2026-08-20T10:12:00.000Z", severity: "Error", count: 10 },
|
|
151
|
+
]),
|
|
152
|
+
);
|
|
153
|
+
patternsMock.mockReturnValue(
|
|
154
|
+
Promise.resolve([
|
|
155
|
+
{
|
|
156
|
+
pattern: "connection refused to <IP>",
|
|
157
|
+
sampleBody: "connection refused to 10.0.0.5",
|
|
158
|
+
count: 8,
|
|
159
|
+
resourceIds: [],
|
|
160
|
+
severities: ["Error"],
|
|
161
|
+
sampleTraceIds: [],
|
|
162
|
+
},
|
|
163
|
+
]),
|
|
164
|
+
);
|
|
165
|
+
eventLinesMock.mockReturnValue({
|
|
166
|
+
lines: [
|
|
167
|
+
{
|
|
168
|
+
date: new Date("2026-08-20T10:05:00.000Z"),
|
|
169
|
+
label: "Deploy: v2.31.0",
|
|
170
|
+
color: "#6366f1",
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
markerCount: 1,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
dispatchSpy = jest
|
|
177
|
+
.spyOn(GlobalEvents, "dispatchEvent")
|
|
178
|
+
.mockImplementation(() => {
|
|
179
|
+
return undefined;
|
|
180
|
+
});
|
|
181
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
182
|
+
getListSpy = jest.spyOn(ModelAPI, "getList").mockReturnValue(
|
|
183
|
+
Promise.resolve({
|
|
184
|
+
data: [buildIncident()],
|
|
185
|
+
count: 1,
|
|
186
|
+
skip: 0,
|
|
187
|
+
limit: 10,
|
|
188
|
+
}) as never,
|
|
189
|
+
);
|
|
190
|
+
createSpy = jest.spyOn(ModelAPI, "create").mockImplementation(((input: {
|
|
191
|
+
model: IncidentInternalNote;
|
|
192
|
+
}) => {
|
|
193
|
+
return Promise.resolve(input.model);
|
|
194
|
+
}) as never);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
afterEach(() => {
|
|
198
|
+
jest.restoreAllMocks();
|
|
199
|
+
cleanup();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
function renderDrawer(): void {
|
|
203
|
+
render(
|
|
204
|
+
<InvestigationDrawer
|
|
205
|
+
window={WINDOW}
|
|
206
|
+
metricViewData={buildViewData()}
|
|
207
|
+
onClose={() => {
|
|
208
|
+
// not exercised here
|
|
209
|
+
}}
|
|
210
|
+
/>,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
describe("findings card", () => {
|
|
215
|
+
test("correlates markers and the log signal into ranked findings", async () => {
|
|
216
|
+
renderDrawer();
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
* The change event leads (its marker came from the hook, scoped to
|
|
220
|
+
* the pinned window)…
|
|
221
|
+
*/
|
|
222
|
+
expect(
|
|
223
|
+
screen.getByText(/Deploy: v2\.31\.0 landed .*before the end/),
|
|
224
|
+
).toBeInTheDocument();
|
|
225
|
+
const hookArgs: Record<string, unknown> = eventLinesMock.mock
|
|
226
|
+
.calls[0]?.[0] as Record<string, unknown>;
|
|
227
|
+
expect((hookArgs["window"] as InBetween<Date>).startValue.getTime()).toBe(
|
|
228
|
+
WINDOW.startValue.getTime(),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// …and once the log signal lands, the error-rate finding joins it.
|
|
232
|
+
await waitFor(() => {
|
|
233
|
+
expect(
|
|
234
|
+
screen.getByText(/10\.0% of the window's log lines/),
|
|
235
|
+
).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("explain with AI", () => {
|
|
241
|
+
test("dispatches the chat-open event with the evidence as the prompt", async () => {
|
|
242
|
+
renderDrawer();
|
|
243
|
+
|
|
244
|
+
await waitFor(() => {
|
|
245
|
+
expect(
|
|
246
|
+
screen.getByText("connection refused to 10.0.0.5"),
|
|
247
|
+
).toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
fireEvent.click(screen.getByText("Explain with AI"));
|
|
251
|
+
|
|
252
|
+
const aiCalls: Array<Array<unknown>> = dispatchSpy.mock.calls.filter(
|
|
253
|
+
(call: Array<unknown>): boolean => {
|
|
254
|
+
return call[0] === EventName.AI_CHAT_TOGGLE;
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
expect(aiCalls).toHaveLength(1);
|
|
258
|
+
const prompt: string = (aiCalls[0]?.[1] as Record<string, string>)[
|
|
259
|
+
"prompt"
|
|
260
|
+
] as string;
|
|
261
|
+
expect(prompt).toContain("host.name = web-01");
|
|
262
|
+
expect(prompt).toContain("connection refused to 10.0.0.5");
|
|
263
|
+
expect(prompt).toContain("Deploy: v2.31.0");
|
|
264
|
+
expect(prompt).toContain("What is the most likely root cause");
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
describe("save to incident", () => {
|
|
269
|
+
test("lists recent incidents, then pins the markdown snapshot as an internal note", async () => {
|
|
270
|
+
renderDrawer();
|
|
271
|
+
|
|
272
|
+
fireEvent.click(screen.getByText("Save to incident"));
|
|
273
|
+
|
|
274
|
+
// The picker fetched the project's recent incidents.
|
|
275
|
+
await waitFor(() => {
|
|
276
|
+
expect(screen.getByText(/INC-42/)).toBeInTheDocument();
|
|
277
|
+
});
|
|
278
|
+
const listArgs: Record<string, unknown> = getListSpy.mock
|
|
279
|
+
.calls[0]?.[0] as Record<string, unknown>;
|
|
280
|
+
expect(listArgs["modelType"]).toBe(Incident);
|
|
281
|
+
expect(listArgs["limit"]).toBe(10);
|
|
282
|
+
|
|
283
|
+
fireEvent.click(screen.getByText("Save note"));
|
|
284
|
+
|
|
285
|
+
await waitFor(() => {
|
|
286
|
+
expect(createSpy).toHaveBeenCalledTimes(1);
|
|
287
|
+
});
|
|
288
|
+
const created: IncidentInternalNote = (
|
|
289
|
+
createSpy.mock.calls[0]?.[0] as { model: IncidentInternalNote }
|
|
290
|
+
).model;
|
|
291
|
+
expect(created.incidentId?.toString()).toBe(INCIDENT_ID.toString());
|
|
292
|
+
expect(created.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
293
|
+
expect(created.note).toContain("### Investigation snapshot");
|
|
294
|
+
expect(created.note).toContain("**Scope:** host.name = web-01");
|
|
295
|
+
expect(created.note).toContain("Open these charts in the Metric Explorer");
|
|
296
|
+
|
|
297
|
+
// Success closes the picker.
|
|
298
|
+
await waitFor(() => {
|
|
299
|
+
expect(screen.queryByText("Save note")).toBeNull();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("cancel closes the picker without any write", async () => {
|
|
304
|
+
renderDrawer();
|
|
305
|
+
|
|
306
|
+
fireEvent.click(screen.getByText("Save to incident"));
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(screen.getByText(/INC-42/)).toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
fireEvent.click(screen.getByText("Cancel"));
|
|
312
|
+
expect(screen.queryByText(/INC-42/)).toBeNull();
|
|
313
|
+
expect(createSpy).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
});
|