@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.
- package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/EnvironmentConfig.ts +15 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/ProjectService.ts +104 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -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/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -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/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
- package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
- package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +19 -12
- package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
- package/UI/Components/Charts/Line/LineChart.tsx +23 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -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/Server/API/EnterpriseLicenseAPI.js +66 -12
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +14 -5
- 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/Index.js +0 -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 +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +88 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- 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/Response.js +2 -2
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +2 -2
- package/build/dist/Server/Utils/StartServer.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/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.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 +12 -4
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.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 +114 -51
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +17 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
- 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,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
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
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
|
+
* The investigation drawer is the "what happened in this window?" panel:
|
|
22
|
+
* it must summarize the log signal for the window+scope, hand every
|
|
23
|
+
* companion signal the SAME pinned window, and route escape hatches
|
|
24
|
+
* (patterns, explorer) through onClose so the page underneath doesn't
|
|
25
|
+
* change behind an open drawer.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const histogramMock: MockFunction = getJestMockFunction();
|
|
29
|
+
const patternsMock: MockFunction = getJestMockFunction();
|
|
30
|
+
const companionTabsMock: MockFunction = getJestMockFunction();
|
|
31
|
+
const embeddedCardMock: MockFunction = getJestMockFunction();
|
|
32
|
+
|
|
33
|
+
jest.mock(
|
|
34
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Logs/LogsInsightsApi",
|
|
35
|
+
() => {
|
|
36
|
+
return {
|
|
37
|
+
__esModule: true,
|
|
38
|
+
fetchLogsHistogramRaw: (...args: Array<any>) => {
|
|
39
|
+
return histogramMock(...args);
|
|
40
|
+
},
|
|
41
|
+
fetchTopErrorPatterns: (...args: Array<any>) => {
|
|
42
|
+
return patternsMock(...args);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/*
|
|
49
|
+
* The companion tabs and the embedded metric card are heavy, separately
|
|
50
|
+
* tested surfaces — here they only need to prove WHAT they were handed.
|
|
51
|
+
*/
|
|
52
|
+
jest.mock(
|
|
53
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Telemetry/TelemetryCompanionSignalTabs",
|
|
54
|
+
() => {
|
|
55
|
+
return {
|
|
56
|
+
__esModule: true,
|
|
57
|
+
default: (props: Record<string, unknown>): React.ReactElement => {
|
|
58
|
+
companionTabsMock(props);
|
|
59
|
+
return React.createElement(
|
|
60
|
+
"div",
|
|
61
|
+
{ "data-testid": "companion-tabs" },
|
|
62
|
+
props["primarySignalElement"] as React.ReactElement,
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
jest.mock(
|
|
70
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Metrics/EmbeddedMetricCard",
|
|
71
|
+
() => {
|
|
72
|
+
return {
|
|
73
|
+
__esModule: true,
|
|
74
|
+
default: (props: Record<string, unknown>): React.ReactElement => {
|
|
75
|
+
embeddedCardMock(props);
|
|
76
|
+
return React.createElement("div", {
|
|
77
|
+
"data-testid": "embedded-metric-card",
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
import InvestigationDrawer from "../../../../App/FeatureSet/Dashboard/src/Components/Telemetry/InvestigationDrawer";
|
|
85
|
+
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
86
|
+
import MetricsAggregationType from "../../../Types/Metrics/MetricsAggregationType";
|
|
87
|
+
import MetricViewData from "../../../Types/Metrics/MetricViewData";
|
|
88
|
+
import TelemetryType from "../../../Types/Telemetry/TelemetryType";
|
|
89
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
90
|
+
|
|
91
|
+
const WINDOW: InBetween<Date> = new InBetween<Date>(
|
|
92
|
+
new Date("2026-08-20T10:00:00.000Z"),
|
|
93
|
+
new Date("2026-08-20T10:15:00.000Z"),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
function buildViewData(): MetricViewData {
|
|
97
|
+
return {
|
|
98
|
+
queryConfigs: [
|
|
99
|
+
{
|
|
100
|
+
metricAliasData: { metricVariable: "a" },
|
|
101
|
+
metricQueryData: {
|
|
102
|
+
filterData: {
|
|
103
|
+
metricName: "cpu.usage",
|
|
104
|
+
attributes: { "host.name": "web-01" },
|
|
105
|
+
aggegationType: MetricsAggregationType.Avg,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
formulaConfigs: [],
|
|
111
|
+
startAndEndDate: null,
|
|
112
|
+
} as unknown as MetricViewData;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let navigateSpy: ReturnType<typeof jest.spyOn>;
|
|
116
|
+
|
|
117
|
+
beforeEach(() => {
|
|
118
|
+
histogramMock.mockReset();
|
|
119
|
+
patternsMock.mockReset();
|
|
120
|
+
companionTabsMock.mockReset();
|
|
121
|
+
embeddedCardMock.mockReset();
|
|
122
|
+
histogramMock.mockReturnValue(
|
|
123
|
+
Promise.resolve([
|
|
124
|
+
{ time: "2026-08-20T10:00:00.000Z", severity: "Information", count: 90 },
|
|
125
|
+
{ time: "2026-08-20T10:05:00.000Z", severity: "Error", count: 10 },
|
|
126
|
+
]),
|
|
127
|
+
);
|
|
128
|
+
patternsMock.mockReturnValue(
|
|
129
|
+
Promise.resolve([
|
|
130
|
+
{
|
|
131
|
+
pattern: "connection refused to <IP>",
|
|
132
|
+
sampleBody: "connection refused to 10.0.0.5",
|
|
133
|
+
count: 42,
|
|
134
|
+
resourceIds: [],
|
|
135
|
+
severities: ["Error"],
|
|
136
|
+
sampleTraceIds: [],
|
|
137
|
+
},
|
|
138
|
+
]),
|
|
139
|
+
);
|
|
140
|
+
navigateSpy = jest.spyOn(Navigation, "navigate").mockImplementation(() => {
|
|
141
|
+
return undefined;
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
afterEach(() => {
|
|
146
|
+
jest.restoreAllMocks();
|
|
147
|
+
cleanup();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("InvestigationDrawer", () => {
|
|
151
|
+
test("summarizes the window's log signal and lists top error patterns", async () => {
|
|
152
|
+
render(
|
|
153
|
+
<InvestigationDrawer
|
|
154
|
+
title="host.name=web-01"
|
|
155
|
+
window={WINDOW}
|
|
156
|
+
metricViewData={buildViewData()}
|
|
157
|
+
onClose={() => {
|
|
158
|
+
// not exercised here
|
|
159
|
+
}}
|
|
160
|
+
/>,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// Scope chips reflect the metric view's filters.
|
|
164
|
+
expect(screen.getByText("host.name = web-01")).toBeInTheDocument();
|
|
165
|
+
|
|
166
|
+
await waitFor(() => {
|
|
167
|
+
expect(screen.getByText("100")).toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
expect(screen.getByText("10")).toBeInTheDocument();
|
|
170
|
+
expect(screen.getByText("10.0%")).toBeInTheDocument();
|
|
171
|
+
expect(
|
|
172
|
+
screen.getByText("connection refused to 10.0.0.5"),
|
|
173
|
+
).toBeInTheDocument();
|
|
174
|
+
|
|
175
|
+
// The histogram request carried the attribute scope.
|
|
176
|
+
const histogramBody: Record<string, unknown> = histogramMock.mock
|
|
177
|
+
.calls[0]?.[0] as Record<string, unknown>;
|
|
178
|
+
expect(
|
|
179
|
+
(histogramBody["attributes"] as Record<string, unknown>)["host.name"],
|
|
180
|
+
).toBe("web-01");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("hands the companion tabs the SAME pinned window and metric queries", async () => {
|
|
184
|
+
render(
|
|
185
|
+
<InvestigationDrawer
|
|
186
|
+
window={WINDOW}
|
|
187
|
+
metricViewData={buildViewData()}
|
|
188
|
+
onClose={() => {
|
|
189
|
+
// not exercised here
|
|
190
|
+
}}
|
|
191
|
+
/>,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
await waitFor(() => {
|
|
195
|
+
expect(companionTabsMock).toHaveBeenCalled();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const tabsProps: Record<string, unknown> = companionTabsMock.mock
|
|
199
|
+
.calls[0]?.[0] as Record<string, unknown>;
|
|
200
|
+
const telemetryQuery: Record<string, unknown> = tabsProps[
|
|
201
|
+
"telemetryQuery"
|
|
202
|
+
] as Record<string, unknown>;
|
|
203
|
+
|
|
204
|
+
expect(telemetryQuery["telemetryType"]).toBe(TelemetryType.Metric);
|
|
205
|
+
const viewData: MetricViewData = telemetryQuery[
|
|
206
|
+
"metricViewData"
|
|
207
|
+
] as MetricViewData;
|
|
208
|
+
expect(viewData.startAndEndDate?.startValue.getTime()).toBe(
|
|
209
|
+
WINDOW.startValue.getTime(),
|
|
210
|
+
);
|
|
211
|
+
// Pinned — never a rolling token.
|
|
212
|
+
expect(viewData.rangeToken).toBeUndefined();
|
|
213
|
+
expect(tabsProps["eventNoun"]).toBe("view");
|
|
214
|
+
|
|
215
|
+
const snapshotWindow: InBetween<Date> = tabsProps[
|
|
216
|
+
"snapshotWindow"
|
|
217
|
+
] as InBetween<Date>;
|
|
218
|
+
expect(snapshotWindow.endValue.getTime()).toBe(WINDOW.endValue.getTime());
|
|
219
|
+
|
|
220
|
+
// The primary element is the interactive metric card, same queries.
|
|
221
|
+
expect(screen.getByTestId("embedded-metric-card")).toBeInTheDocument();
|
|
222
|
+
const cardProps: Record<string, unknown> = embeddedCardMock.mock
|
|
223
|
+
.calls[0]?.[0] as Record<string, unknown>;
|
|
224
|
+
const cardConfigs: Array<Record<string, any>> = cardProps[
|
|
225
|
+
"queryConfigs"
|
|
226
|
+
] as Array<Record<string, any>>;
|
|
227
|
+
expect(
|
|
228
|
+
cardConfigs[0]?.["metricQueryData"]["filterData"]["metricName"],
|
|
229
|
+
).toBe("cpu.usage");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("escape hatches close the drawer before navigating", async () => {
|
|
233
|
+
const onClose: MockFunction = getJestMockFunction();
|
|
234
|
+
|
|
235
|
+
render(
|
|
236
|
+
<InvestigationDrawer
|
|
237
|
+
window={WINDOW}
|
|
238
|
+
metricViewData={buildViewData()}
|
|
239
|
+
onClose={(...args: Array<any>) => {
|
|
240
|
+
onClose(...args);
|
|
241
|
+
}}
|
|
242
|
+
/>,
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
fireEvent.click(screen.getByText("Open in Metric Explorer"));
|
|
246
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
247
|
+
expect(navigateSpy).toHaveBeenCalledTimes(1);
|
|
248
|
+
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(
|
|
251
|
+
screen.getByText("connection refused to 10.0.0.5"),
|
|
252
|
+
).toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
fireEvent.click(screen.getByText("connection refused to 10.0.0.5"));
|
|
255
|
+
expect(onClose).toHaveBeenCalledTimes(2);
|
|
256
|
+
expect(navigateSpy).toHaveBeenCalledTimes(2);
|
|
257
|
+
});
|
|
258
|
+
});
|