@oneuptime/common 14.0.2 → 14.0.3
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/DatabaseModels/AlertFeed.ts +2 -0
- package/Models/DatabaseModels/IncidentAlert.ts +540 -0
- package/Models/DatabaseModels/IncidentFeed.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/Project.ts +60 -0
- package/Models/DatabaseModels/RumApplication.ts +52 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/IncidentAlertService.ts +1811 -0
- package/Server/Services/IncidentService.ts +210 -10
- package/Server/Services/IncidentStateTimelineService.ts +24 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
- package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
- package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
- package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
- package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
- package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
- package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
- package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
- package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
- package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
- package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
- package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
- package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
- package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
- package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
- package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
- package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
- package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
- package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
- package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
- package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
- package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
- package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
- package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
- package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
- package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
- package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
- package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
- package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
- package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
- package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
- package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
- package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
- package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
- package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
- package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
- package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
- package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
- package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
- package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
- package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
- package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
- package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
- package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
- package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
- package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
- package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
- package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
- package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
- package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
- package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
- package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
- package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
- package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
- package/Tests/Types/Rum/WebVitals.test.ts +173 -0
- package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
- package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
- package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
- package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
- package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
- package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
- package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
- package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
- package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
- package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
- package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
- package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
- package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
- package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
- package/Types/Incident/IncidentAlertLink.ts +38 -0
- package/Types/Permission.ts +48 -0
- package/Types/Rum/SessionReplay.ts +25 -6
- package/Types/Rum/WebVitals.ts +52 -0
- package/UI/Components/Detail/FieldLabel.tsx +8 -0
- package/UI/Components/InfoCard/InfoCard.tsx +47 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
- package/UI/Components/ModelTable/Column.ts +5 -0
- package/UI/Components/Table/TableHeader.tsx +47 -2
- package/UI/Components/Table/Types/Column.ts +7 -0
- package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
- package/UI/Utils/PermissionGate.ts +11 -2
- package/Utils/Incident/IncidentFromAlerts.ts +516 -0
- package/Utils/Rum/SessionTraceState.ts +212 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
- package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/IncidentFeed.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/Project.js +62 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
- package/build/dist/Server/Services/IncidentService.js +164 -12
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +25 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
- package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
- package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +18 -2
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
- package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
- package/build/dist/UI/Utils/PermissionGate.js +4 -2
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
- package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
- package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
jest,
|
|
7
|
+
test,
|
|
8
|
+
} from "@jest/globals";
|
|
9
|
+
import "@testing-library/jest-dom";
|
|
10
|
+
import {
|
|
11
|
+
act,
|
|
12
|
+
cleanup,
|
|
13
|
+
fireEvent,
|
|
14
|
+
render,
|
|
15
|
+
screen,
|
|
16
|
+
} from "@testing-library/react";
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
import { MemoryRouter } from "react-router-dom";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* The (i) tooltips on the Serverless function overview: the texts
|
|
22
|
+
* themselves (rules plus what the page really computes), and the page,
|
|
23
|
+
* rendered for real over a fake API, showing each text beside the right
|
|
24
|
+
* tile and chart.
|
|
25
|
+
*
|
|
26
|
+
* Everything the page fetches through is stubbed inline - jest.mock is
|
|
27
|
+
* hoisted above the imports, so the factories may only close over names
|
|
28
|
+
* that start with "mock".
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
interface MockAggregateCall {
|
|
32
|
+
modelType: { name: string };
|
|
33
|
+
aggregateBy: {
|
|
34
|
+
aggregationType: string;
|
|
35
|
+
query: Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface MockCountCall {
|
|
40
|
+
modelType: { name: string };
|
|
41
|
+
query: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mockAggregateCalls: Array<MockAggregateCall> = [];
|
|
45
|
+
const mockCountCalls: Array<MockCountCall> = [];
|
|
46
|
+
let mockInstanceCount: number = 7;
|
|
47
|
+
// Keep the span aggregates and the instance count pending forever.
|
|
48
|
+
let mockHold: boolean = false;
|
|
49
|
+
|
|
50
|
+
jest.mock("../../../UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI", () => {
|
|
51
|
+
return {
|
|
52
|
+
__esModule: true,
|
|
53
|
+
default: {
|
|
54
|
+
aggregate: (call: MockAggregateCall): Promise<unknown> => {
|
|
55
|
+
mockAggregateCalls.push(call);
|
|
56
|
+
if (mockHold) {
|
|
57
|
+
return new Promise<unknown>(() => {});
|
|
58
|
+
}
|
|
59
|
+
const at: Date = new Date("2026-09-24T10:00:00.000Z");
|
|
60
|
+
const type: string = call.aggregateBy.aggregationType;
|
|
61
|
+
|
|
62
|
+
if (type === "P95") {
|
|
63
|
+
// 250 ms, in nanoseconds.
|
|
64
|
+
return Promise.resolve({ data: [{ timestamp: at, value: 2.5e8 }] });
|
|
65
|
+
}
|
|
66
|
+
if (call.aggregateBy.query["statusCode"] !== undefined) {
|
|
67
|
+
return Promise.resolve({ data: [{ timestamp: at, value: 3 }] });
|
|
68
|
+
}
|
|
69
|
+
return Promise.resolve({ data: [{ timestamp: at, value: 100 }] });
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
76
|
+
return {
|
|
77
|
+
__esModule: true,
|
|
78
|
+
default: {
|
|
79
|
+
getItem: (): Promise<unknown> => {
|
|
80
|
+
return Promise.resolve({
|
|
81
|
+
name: "checkout-handler",
|
|
82
|
+
functionIdentifier: "checkout-handler",
|
|
83
|
+
otelCollectorStatus: "connected",
|
|
84
|
+
cloudPlatform: "aws_lambda",
|
|
85
|
+
cloudRegion: "us-east-1",
|
|
86
|
+
runtimeName: "nodejs",
|
|
87
|
+
runtimeVersion: "20",
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
count: (call: MockCountCall): Promise<number> => {
|
|
91
|
+
mockCountCalls.push(call);
|
|
92
|
+
if (mockHold) {
|
|
93
|
+
return new Promise<number>(() => {});
|
|
94
|
+
}
|
|
95
|
+
return Promise.resolve(mockInstanceCount);
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
jest.mock("../../../UI/Utils/Project", () => {
|
|
102
|
+
const ObjectIDModule: { default: new (id: string) => unknown } =
|
|
103
|
+
jest.requireActual("../../../Types/ObjectID") as {
|
|
104
|
+
default: new (id: string) => unknown;
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
__esModule: true,
|
|
108
|
+
default: {
|
|
109
|
+
getCurrentProjectId: (): unknown => {
|
|
110
|
+
return new ObjectIDModule.default(
|
|
111
|
+
"aaaaaaaa-0000-4000-8000-000000000001",
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
jest.mock("../../../UI/Utils/Navigation", () => {
|
|
119
|
+
const ObjectIDModule: { default: new (id: string) => unknown } =
|
|
120
|
+
jest.requireActual("../../../Types/ObjectID") as {
|
|
121
|
+
default: new (id: string) => unknown;
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
__esModule: true,
|
|
125
|
+
default: {
|
|
126
|
+
getLastParamAsObjectID: (): unknown => {
|
|
127
|
+
return new ObjectIDModule.default(
|
|
128
|
+
"bbbbbbbb-0000-4000-8000-000000000002",
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
136
|
+
return {
|
|
137
|
+
__esModule: true,
|
|
138
|
+
default: {
|
|
139
|
+
getFriendlyMessage: (): string => {
|
|
140
|
+
return "failed";
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
jest.mock("react-i18next", () => {
|
|
147
|
+
return {
|
|
148
|
+
useTranslation: () => {
|
|
149
|
+
return {
|
|
150
|
+
t: (value: string): string => {
|
|
151
|
+
return value;
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Recharts does not lay out in jsdom; what is under test is the header.
|
|
159
|
+
jest.mock("../../../UI/Components/Charts/Line/LineChart", () => {
|
|
160
|
+
return {
|
|
161
|
+
__esModule: true,
|
|
162
|
+
default: (): React.ReactElement => {
|
|
163
|
+
return React.createElement("div", { "data-testid": "line-chart" });
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
jest.mock(
|
|
169
|
+
"../../../UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker",
|
|
170
|
+
() => {
|
|
171
|
+
return {
|
|
172
|
+
__esModule: true,
|
|
173
|
+
default: (): null => {
|
|
174
|
+
return null;
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
jest.mock(
|
|
181
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/TelemetryResource/AutoRefreshControl",
|
|
182
|
+
() => {
|
|
183
|
+
return {
|
|
184
|
+
__esModule: true,
|
|
185
|
+
default: (): null => {
|
|
186
|
+
return null;
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
import ServerlessFunctionOverview from "../../../../App/FeatureSet/Dashboard/src/Pages/Serverless/View/Overview";
|
|
193
|
+
import {
|
|
194
|
+
SERVERLESS_METRIC_DESCRIPTIONS,
|
|
195
|
+
ServerlessMetric,
|
|
196
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/ServerlessMetricDescriptions";
|
|
197
|
+
import {
|
|
198
|
+
expectReadableDescriptionRecord,
|
|
199
|
+
expectTitleExplained,
|
|
200
|
+
} from "./MetricDescriptionRules";
|
|
201
|
+
import PageComponentProps from "../../../../App/FeatureSet/Dashboard/src/Pages/PageComponentProps";
|
|
202
|
+
import Route from "../../../Types/API/Route";
|
|
203
|
+
|
|
204
|
+
const PAGE_PROPS: PageComponentProps = {
|
|
205
|
+
pageRoute: new Route("/serverless/overview"),
|
|
206
|
+
currentProject: null,
|
|
207
|
+
hasPaymentMethod: false,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const D: Record<ServerlessMetric, string> = SERVERLESS_METRIC_DESCRIPTIONS;
|
|
211
|
+
|
|
212
|
+
// Title on the page -> the text its (i) must show, in page order.
|
|
213
|
+
const TILES: Array<[string, ServerlessMetric]> = [
|
|
214
|
+
["Invocations", "invocations"],
|
|
215
|
+
["Error rate", "errorRate"],
|
|
216
|
+
["p95 duration", "p95Duration"],
|
|
217
|
+
["Instances", "instances"],
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
const CHARTS: Array<[string, ServerlessMetric]> = [
|
|
221
|
+
["Invocations", "invocationsChart"],
|
|
222
|
+
["p95 duration", "p95DurationChart"],
|
|
223
|
+
];
|
|
224
|
+
|
|
225
|
+
describe("SERVERLESS_METRIC_DESCRIPTIONS", () => {
|
|
226
|
+
test("every text reads as a short, finished, jargon-explaining sentence", () => {
|
|
227
|
+
expectReadableDescriptionRecord(D, "SERVERLESS_METRIC_DESCRIPTIONS");
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("covers exactly the tiles and charts on the page", () => {
|
|
231
|
+
expect(
|
|
232
|
+
[...TILES, ...CHARTS]
|
|
233
|
+
.map((entry: [string, ServerlessMetric]): string => {
|
|
234
|
+
return entry[1];
|
|
235
|
+
})
|
|
236
|
+
.sort(),
|
|
237
|
+
).toEqual(Object.keys(D).sort());
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test.each([...TILES, ...CHARTS])(
|
|
241
|
+
"the %s text explains any percentile its title names",
|
|
242
|
+
(title: string, key: ServerlessMetric) => {
|
|
243
|
+
expectTitleExplained(title, D[key]);
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
test("Invocations is honest that it counts spans, not invocations", () => {
|
|
248
|
+
// fetchSpanMetrics has no root-span filter: every span with the faas.name.
|
|
249
|
+
expect(D.invocations).toMatch(/spans/i);
|
|
250
|
+
expect(D.invocations).toContain("faas.name");
|
|
251
|
+
expect(D.invocations).toMatch(/higher than the true invocation count/);
|
|
252
|
+
expect(D.invocationsChart).toMatch(/several spans/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("the p95 tile says it is an average of per-interval p95s", () => {
|
|
256
|
+
expect(D.p95Duration).toMatch(/95%/);
|
|
257
|
+
expect(D.p95Duration).toContain(
|
|
258
|
+
"Worked out for each interval on the chart",
|
|
259
|
+
);
|
|
260
|
+
expect(D.p95Duration).toMatch(/averaged over the selected range/);
|
|
261
|
+
// The Service, Cloud and RUM p95 tiles say the same.
|
|
262
|
+
expect(D.p95Duration).toContain(
|
|
263
|
+
"so quiet and busy intervals count equally",
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("the p95 chart is per interval and does not claim a range average", () => {
|
|
268
|
+
expect(D.p95DurationChart).toMatch(/in each interval/);
|
|
269
|
+
expect(D.p95DurationChart).not.toMatch(/averaged/);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("Error rate names the bar's thresholds the page actually uses", () => {
|
|
273
|
+
expect(D.errorRate).toContain("amber at 1%");
|
|
274
|
+
expect(D.errorRate).toContain("red at 5%");
|
|
275
|
+
expect(D.errorRate).toMatch(/status was set to Error/);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("Instances says it ignores the time picker and how rows age out", () => {
|
|
279
|
+
// ModelAPI.count with no lastSeenAt filter; the cleanup job prunes rows.
|
|
280
|
+
expect(D.instances).toMatch(/whatever the selected range/);
|
|
281
|
+
expect(D.instances).toMatch(/15 minutes/);
|
|
282
|
+
expect(D.instances).toMatch(/idle function keeps its last list/);
|
|
283
|
+
/*
|
|
284
|
+
* Serverless:CleanupStaleResources prunes only CONNECTED functions, and
|
|
285
|
+
* anchors the cutoff to the function's own lastSeenAt - so an
|
|
286
|
+
* environment goes only while the function keeps reporting.
|
|
287
|
+
*/
|
|
288
|
+
expect(D.instances).toMatch(/while the function keeps reporting/);
|
|
289
|
+
expect(D.instances).toContain("faas.instance");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("Invocations does not promise a span per invocation", () => {
|
|
293
|
+
// Nothing in the fetch guarantees one; the text must hedge.
|
|
294
|
+
expect(D.invocations).toMatch(/usually produces one span/);
|
|
295
|
+
expect(D.invocations).not.toMatch(/at least one span/);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
async function hover(trigger: HTMLElement): Promise<void> {
|
|
300
|
+
fireEvent.mouseEnter(trigger);
|
|
301
|
+
await act(async () => {
|
|
302
|
+
jest.advanceTimersByTime(200);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function renderOverview(): Promise<void> {
|
|
307
|
+
render(
|
|
308
|
+
<MemoryRouter>
|
|
309
|
+
<ServerlessFunctionOverview {...PAGE_PROPS} />
|
|
310
|
+
</MemoryRouter>,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// The instance count is the last thing the page fetches.
|
|
314
|
+
await screen.findByText(String(mockInstanceCount));
|
|
315
|
+
await screen.findByText("250 ms");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function infoButtons(title: string): Array<HTMLElement> {
|
|
319
|
+
return screen.getAllByRole("button", { name: `About ${title}` });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
describe("Serverless function overview page", () => {
|
|
323
|
+
beforeEach(() => {
|
|
324
|
+
jest.useFakeTimers();
|
|
325
|
+
mockAggregateCalls.length = 0;
|
|
326
|
+
mockCountCalls.length = 0;
|
|
327
|
+
mockInstanceCount = 7;
|
|
328
|
+
mockHold = false;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
afterEach(() => {
|
|
332
|
+
cleanup();
|
|
333
|
+
jest.useRealTimers();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("renders one (i) per tile and per chart, and no more", async () => {
|
|
337
|
+
await renderOverview();
|
|
338
|
+
|
|
339
|
+
const all: Array<HTMLElement> = screen.getAllByRole("button", {
|
|
340
|
+
name: /^About /,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(all).toHaveLength(TILES.length + CHARTS.length);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test.each(TILES)(
|
|
347
|
+
"the %s tile explains itself on hover",
|
|
348
|
+
async (title: string, key: ServerlessMetric) => {
|
|
349
|
+
await renderOverview();
|
|
350
|
+
|
|
351
|
+
// Tiles render before the charts, so the tile's (i) is the first.
|
|
352
|
+
await hover(infoButtons(title)[0]!);
|
|
353
|
+
|
|
354
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(D[key]);
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
test.each(CHARTS)(
|
|
359
|
+
"the %s chart explains itself on hover",
|
|
360
|
+
async (title: string, key: ServerlessMetric) => {
|
|
361
|
+
await renderOverview();
|
|
362
|
+
|
|
363
|
+
const buttons: Array<HTMLElement> = infoButtons(title);
|
|
364
|
+
|
|
365
|
+
await hover(buttons[buttons.length - 1]!);
|
|
366
|
+
|
|
367
|
+
expect(screen.getByRole("tooltip")).toHaveTextContent(D[key]);
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
test("the loading tiles and charts keep their (i)", async () => {
|
|
372
|
+
mockHold = true;
|
|
373
|
+
|
|
374
|
+
render(
|
|
375
|
+
<MemoryRouter>
|
|
376
|
+
<ServerlessFunctionOverview {...PAGE_PROPS} />
|
|
377
|
+
</MemoryRouter>,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await screen.findByText("Loading Invocations");
|
|
381
|
+
|
|
382
|
+
for (const [title, key] of TILES) {
|
|
383
|
+
expect(screen.getByText(`Loading ${title}`)).toBeInTheDocument();
|
|
384
|
+
|
|
385
|
+
const button: HTMLElement = infoButtons(title)[0]!;
|
|
386
|
+
|
|
387
|
+
await hover(button);
|
|
388
|
+
|
|
389
|
+
const tooltips: Array<HTMLElement> = screen.getAllByRole("tooltip");
|
|
390
|
+
|
|
391
|
+
expect(tooltips[tooltips.length - 1]).toHaveTextContent(D[key]);
|
|
392
|
+
fireEvent.mouseLeave(button);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const [title, key] of CHARTS) {
|
|
396
|
+
const buttons: Array<HTMLElement> = infoButtons(title);
|
|
397
|
+
const button: HTMLElement = buttons[buttons.length - 1]!;
|
|
398
|
+
|
|
399
|
+
await hover(button);
|
|
400
|
+
|
|
401
|
+
const tooltips: Array<HTMLElement> = screen.getAllByRole("tooltip");
|
|
402
|
+
|
|
403
|
+
expect(tooltips[tooltips.length - 1]).toHaveTextContent(D[key]);
|
|
404
|
+
fireEvent.mouseLeave(button);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("the Instances count it explains is not bounded by the time range", async () => {
|
|
409
|
+
await renderOverview();
|
|
410
|
+
|
|
411
|
+
const instanceCounts: Array<MockCountCall> = mockCountCalls.filter(
|
|
412
|
+
(call: MockCountCall): boolean => {
|
|
413
|
+
return call.modelType.name === "ServerlessFunctionInstance";
|
|
414
|
+
},
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
expect(instanceCounts).toHaveLength(1);
|
|
418
|
+
// Only the function, no lastSeenAt window: "whatever the selected range".
|
|
419
|
+
expect(Object.keys(instanceCounts[0]!.query)).toEqual([
|
|
420
|
+
"serverlessFunctionId",
|
|
421
|
+
]);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("no (i) sits inside a link or another button", async () => {
|
|
425
|
+
await renderOverview();
|
|
426
|
+
|
|
427
|
+
for (const button of screen.getAllByRole("button", { name: /^About / })) {
|
|
428
|
+
expect(button.closest("a")).toBeNull();
|
|
429
|
+
expect(button.parentElement?.closest("button") ?? null).toBeNull();
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("the linked Instances tile still links, beside its (i)", async () => {
|
|
434
|
+
await renderOverview();
|
|
435
|
+
|
|
436
|
+
const link: HTMLElement = screen.getByRole("link", {
|
|
437
|
+
name: "View Instances",
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
expect(link.getAttribute("href")).toMatch(/instances/);
|
|
441
|
+
|
|
442
|
+
const tile: HTMLElement = link.parentElement as HTMLElement;
|
|
443
|
+
|
|
444
|
+
expect(
|
|
445
|
+
tile.querySelector('button[aria-label="About Instances"]'),
|
|
446
|
+
).not.toBeNull();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("the span metrics it explains are scoped by faas.name alone", async () => {
|
|
450
|
+
/*
|
|
451
|
+
* The Invocations text says "matched by its faas.name attribute"; pin
|
|
452
|
+
* that the fetch really is that scope, with no primary-entity filter.
|
|
453
|
+
*/
|
|
454
|
+
await renderOverview();
|
|
455
|
+
|
|
456
|
+
const spanCalls: Array<MockAggregateCall> = mockAggregateCalls.filter(
|
|
457
|
+
(call: MockAggregateCall): boolean => {
|
|
458
|
+
return call.modelType.name === "Span";
|
|
459
|
+
},
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
expect(spanCalls.length).toBeGreaterThan(0);
|
|
463
|
+
|
|
464
|
+
for (const call of spanCalls) {
|
|
465
|
+
expect(call.aggregateBy.query["attributes"]).toEqual({
|
|
466
|
+
"resource.faas.name": "checkout-handler",
|
|
467
|
+
});
|
|
468
|
+
expect(call.aggregateBy.query["primaryEntityId"]).toBeUndefined();
|
|
469
|
+
// Every span, not only root or server spans.
|
|
470
|
+
expect(call.aggregateBy.query["kind"]).toBeUndefined();
|
|
471
|
+
expect(call.aggregateBy.query["parentSpanId"]).toBeUndefined();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
});
|