@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
|
@@ -11,7 +11,8 @@ import PrivacySummaryCard, {
|
|
|
11
11
|
* Five sentences that say what a recording contains for the person being
|
|
12
12
|
* recorded, one per masking / identity / location / retention / consent
|
|
13
13
|
* choice, each with a "Change" link; a field that was never set reads as
|
|
14
|
-
* the default it actually is.
|
|
14
|
+
* the default it actually is. A sixth ("backend-link") joins them while
|
|
15
|
+
* same-origin trace propagation is on, which is the default.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
function sentence(
|
|
@@ -29,23 +30,40 @@ function sentence(
|
|
|
29
30
|
return found as PrivacySummarySentence;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
},
|
|
38
|
-
);
|
|
33
|
+
function keysOf(sentences: Array<PrivacySummarySentence>): Array<string> {
|
|
34
|
+
return sentences.map((entry: PrivacySummarySentence): string => {
|
|
35
|
+
return entry.key;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
describe("buildPrivacySummary", () => {
|
|
40
|
+
it("produces six sentences in the reviewer's order while same-origin trace propagation is on (the default)", () => {
|
|
41
|
+
expect(keysOf(buildPrivacySummary({}))).toEqual([
|
|
41
42
|
"page-text",
|
|
42
43
|
"identity",
|
|
44
|
+
"backend-link",
|
|
45
|
+
"location",
|
|
46
|
+
"retention",
|
|
47
|
+
"consent",
|
|
48
|
+
]);
|
|
49
|
+
expect(
|
|
50
|
+
keysOf(buildPrivacySummary({ sameOriginTracePropagation: true })),
|
|
51
|
+
).toEqual([
|
|
52
|
+
"page-text",
|
|
53
|
+
"identity",
|
|
54
|
+
"backend-link",
|
|
43
55
|
"location",
|
|
44
56
|
"retention",
|
|
45
57
|
"consent",
|
|
46
58
|
]);
|
|
47
59
|
});
|
|
48
60
|
|
|
61
|
+
it("produces exactly the five sentences when same-origin trace propagation is off", () => {
|
|
62
|
+
expect(
|
|
63
|
+
keysOf(buildPrivacySummary({ sameOriginTracePropagation: false })),
|
|
64
|
+
).toEqual(["page-text", "identity", "location", "retention", "consent"]);
|
|
65
|
+
});
|
|
66
|
+
|
|
49
67
|
it("an empty policy reads as the model defaults and marks every sentence default", () => {
|
|
50
68
|
const sentences: Array<PrivacySummarySentence> = buildPrivacySummary({});
|
|
51
69
|
|
|
@@ -105,6 +123,103 @@ describe("buildPrivacySummary", () => {
|
|
|
105
123
|
).toContain("least private mode");
|
|
106
124
|
});
|
|
107
125
|
|
|
126
|
+
/*
|
|
127
|
+
* Same-origin trace propagation puts the session id on the page's own
|
|
128
|
+
* requests. The sentence says where it goes (the backend and every
|
|
129
|
+
* service it calls) and what that means (backend telemetry naming the
|
|
130
|
+
* user leads to the recording), and is flagged for a second look.
|
|
131
|
+
*/
|
|
132
|
+
it("backend link on reads as the session id reaching the backend and beyond, and is flagged", () => {
|
|
133
|
+
const on: PrivacySummarySentence = sentence(
|
|
134
|
+
buildPrivacySummary({ sameOriginTracePropagation: true }),
|
|
135
|
+
"backend-link",
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(on.text).toContain(
|
|
139
|
+
"Same-origin requests carry this session's id to your backend",
|
|
140
|
+
);
|
|
141
|
+
expect(on.text).toContain("forwards it to the services it calls");
|
|
142
|
+
expect(on.text).toContain("third parties included");
|
|
143
|
+
expect(on.text).toContain(
|
|
144
|
+
"Backend telemetry that names the user links to the recording",
|
|
145
|
+
);
|
|
146
|
+
expect(on.text).toContain("The visitor id is never sent");
|
|
147
|
+
expect(on.isSensitive).toBe(true);
|
|
148
|
+
expect(on.isDefault).toBe(false);
|
|
149
|
+
expect(on.changeLabel).toBe("Change trace propagation");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("a never-set switch reads as the on default and says so", () => {
|
|
153
|
+
for (const value of [undefined, null]) {
|
|
154
|
+
const backendLink: PrivacySummarySentence = sentence(
|
|
155
|
+
buildPrivacySummary({ sameOriginTracePropagation: value }),
|
|
156
|
+
"backend-link",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(backendLink.isDefault).toBe(true);
|
|
160
|
+
expect(backendLink.isSensitive).toBe(true);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/*
|
|
165
|
+
* With identity capture off the recording stores no user reference, but
|
|
166
|
+
* a linked backend span or log that names the user still leads to it, so
|
|
167
|
+
* "cannot be found by who the person was" would overpromise.
|
|
168
|
+
*/
|
|
169
|
+
it("identity off with the backend link on does not promise the session cannot be found by who the person was", () => {
|
|
170
|
+
const identity: PrivacySummarySentence = sentence(
|
|
171
|
+
buildPrivacySummary({
|
|
172
|
+
captureUserIdentity: false,
|
|
173
|
+
sameOriginTracePropagation: true,
|
|
174
|
+
}),
|
|
175
|
+
"identity",
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
expect(identity.text).toContain("pseudonymous");
|
|
179
|
+
expect(identity.text).toContain(
|
|
180
|
+
"Backend telemetry linked to it by same-origin trace propagation can still name the person",
|
|
181
|
+
);
|
|
182
|
+
expect(identity.text).not.toContain(
|
|
183
|
+
"cannot be found by who the person was",
|
|
184
|
+
);
|
|
185
|
+
expect(identity.isSensitive).toBe(false);
|
|
186
|
+
|
|
187
|
+
/* The never-set switch is the on default, so the caveat applies there too. */
|
|
188
|
+
expect(
|
|
189
|
+
sentence(buildPrivacySummary({ captureUserIdentity: false }), "identity")
|
|
190
|
+
.text,
|
|
191
|
+
).toContain("can still name the person");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("identity off with the backend link off keeps the full pseudonymity promise", () => {
|
|
195
|
+
const identity: PrivacySummarySentence = sentence(
|
|
196
|
+
buildPrivacySummary({
|
|
197
|
+
captureUserIdentity: false,
|
|
198
|
+
sameOriginTracePropagation: false,
|
|
199
|
+
}),
|
|
200
|
+
"identity",
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
expect(identity.text).toContain(
|
|
204
|
+
"so a session cannot be found by who the person was",
|
|
205
|
+
);
|
|
206
|
+
expect(identity.text).not.toContain("Backend telemetry");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("identity on is unchanged by the backend link", () => {
|
|
210
|
+
for (const value of [true, false]) {
|
|
211
|
+
expect(
|
|
212
|
+
sentence(
|
|
213
|
+
buildPrivacySummary({
|
|
214
|
+
captureUserIdentity: true,
|
|
215
|
+
sameOriginTracePropagation: value,
|
|
216
|
+
}),
|
|
217
|
+
"identity",
|
|
218
|
+
).text,
|
|
219
|
+
).toContain("Recordings are identified");
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
108
223
|
it("identity off reads as pseudonymous and is not flagged", () => {
|
|
109
224
|
const off: PrivacySummarySentence = sentence(
|
|
110
225
|
buildPrivacySummary({ captureUserIdentity: false }),
|
|
@@ -192,6 +307,48 @@ describe("buildPrivacySummary", () => {
|
|
|
192
307
|
});
|
|
193
308
|
|
|
194
309
|
describe("PrivacySummaryCard", () => {
|
|
310
|
+
it("renders the backend-link row with its Change link while same-origin trace propagation is on", () => {
|
|
311
|
+
render(
|
|
312
|
+
<PrivacySummaryCard
|
|
313
|
+
policy={{ sameOriginTracePropagation: true }}
|
|
314
|
+
changeHref="#replay-policy"
|
|
315
|
+
/>,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(
|
|
319
|
+
screen.getByTestId("privacy-summary-backend-link"),
|
|
320
|
+
).toHaveTextContent(
|
|
321
|
+
"Same-origin requests carry this session's id to your backend",
|
|
322
|
+
);
|
|
323
|
+
expect(screen.getByTestId("privacy-summary-backend-link")).toHaveAttribute(
|
|
324
|
+
"data-default",
|
|
325
|
+
"false",
|
|
326
|
+
);
|
|
327
|
+
expect(
|
|
328
|
+
screen.getByTestId("privacy-summary-change-backend-link"),
|
|
329
|
+
).toHaveAttribute("href", "#replay-policy");
|
|
330
|
+
expect(
|
|
331
|
+
screen.getByTestId("privacy-summary-change-backend-link"),
|
|
332
|
+
).toHaveTextContent("Change trace propagation");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("renders no backend-link row when same-origin trace propagation is off", () => {
|
|
336
|
+
render(
|
|
337
|
+
<PrivacySummaryCard
|
|
338
|
+
policy={{
|
|
339
|
+
sameOriginTracePropagation: false,
|
|
340
|
+
captureUserIdentity: false,
|
|
341
|
+
}}
|
|
342
|
+
changeHref="#replay-policy"
|
|
343
|
+
/>,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(screen.queryByTestId("privacy-summary-backend-link")).toBeNull();
|
|
347
|
+
expect(screen.getByTestId("privacy-summary-identity")).toHaveTextContent(
|
|
348
|
+
"cannot be found by who the person was",
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
195
352
|
it("renders five rows, each with a Change link to the policy anchor, and marks defaults", () => {
|
|
196
353
|
render(
|
|
197
354
|
<PrivacySummaryCard
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
act,
|
|
4
|
+
cleanup,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
within,
|
|
9
|
+
} from "@testing-library/react";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
import { MemoryRouter } from "react-router-dom";
|
|
12
|
+
import {
|
|
13
|
+
afterEach,
|
|
14
|
+
beforeEach,
|
|
15
|
+
describe,
|
|
16
|
+
expect,
|
|
17
|
+
jest,
|
|
18
|
+
test,
|
|
19
|
+
} from "@jest/globals";
|
|
20
|
+
import HTTPErrorResponse from "../../../Types/API/HTTPErrorResponse";
|
|
21
|
+
import { RecordingHealthStatus } from "../../../Types/Rum/SessionReplayHealth";
|
|
22
|
+
import {
|
|
23
|
+
diagnoseRecordingHealth,
|
|
24
|
+
SESSION_REPLAY_RECORDER_ACTIVE_WINDOW_MS,
|
|
25
|
+
SESSION_REPLAY_REFUSAL_ALERT_THRESHOLD,
|
|
26
|
+
SESSION_REPLAY_STALE_CHUNK_MS,
|
|
27
|
+
} from "../../../Utils/Rum/SessionReplayHealth";
|
|
28
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* The Replay Health page's (i) tooltips: the four pipeline stages, the two
|
|
32
|
+
* upload counters and the two byte meters. The policy and recorder panels
|
|
33
|
+
* list settings and versions, not measurements, and carry none; nor does
|
|
34
|
+
* the compact card on the Replay Policy page, which only shows the
|
|
35
|
+
* diagnosis.
|
|
36
|
+
*
|
|
37
|
+
* Each (i) is hovered and must show its own text from
|
|
38
|
+
* RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS. The thresholds the texts name
|
|
39
|
+
* (amber after 24 hours, after 6 hours, from 80%, "5 or more") are checked
|
|
40
|
+
* against what the page actually colours, on both sides of each line.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
44
|
+
|
|
45
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
46
|
+
return {
|
|
47
|
+
__esModule: true,
|
|
48
|
+
default: {
|
|
49
|
+
post: (...args: Array<unknown>) => {
|
|
50
|
+
return postMock(...args);
|
|
51
|
+
},
|
|
52
|
+
getFriendlyMessage: (error: unknown): string => {
|
|
53
|
+
return error instanceof HTTPErrorResponse
|
|
54
|
+
? error.message
|
|
55
|
+
: String(error);
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
62
|
+
return {
|
|
63
|
+
__esModule: true,
|
|
64
|
+
default: {
|
|
65
|
+
getCommonHeaders: (): Record<string, string> => {
|
|
66
|
+
return {};
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
import {
|
|
73
|
+
PIPELINE_STAGE_TOOLTIPS,
|
|
74
|
+
RecordingHealthDashboardView,
|
|
75
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/RecordingHealthDashboard";
|
|
76
|
+
import { RecordingHealthSummaryView } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/RecordingHealthCard";
|
|
77
|
+
import { USAGE_WARNING_PERCENT } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/RecordingHealthModel";
|
|
78
|
+
import { SessionReplayHealthSnapshot } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/useSessionReplayHealth";
|
|
79
|
+
import {
|
|
80
|
+
RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS,
|
|
81
|
+
RumRecordingHealthMetric,
|
|
82
|
+
} from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/RumMetricDescriptions";
|
|
83
|
+
import { expectReadableDescriptionRecord } from "../../App/Dashboard/MetricDescriptionRules";
|
|
84
|
+
|
|
85
|
+
const APP_ID: string = "0193c0de-1111-4aaa-8bbb-000000000001";
|
|
86
|
+
const NOW: number = Date.parse("2026-09-05T10:00:00.000Z");
|
|
87
|
+
const MINUTE_MS: number = 60 * 1000;
|
|
88
|
+
const HOUR_MS: number = 60 * MINUTE_MS;
|
|
89
|
+
const MB: number = 1024 * 1024;
|
|
90
|
+
const GB: number = 1024 * MB;
|
|
91
|
+
// Every (i) is a button named "About <what it explains>".
|
|
92
|
+
const INFO_BUTTON_NAME: RegExp = /^About /;
|
|
93
|
+
|
|
94
|
+
function iso(offsetMs: number): string {
|
|
95
|
+
return new Date(NOW - offsetMs).toISOString();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function makeStatus(
|
|
99
|
+
overrides?: Partial<RecordingHealthStatus>,
|
|
100
|
+
): RecordingHealthStatus {
|
|
101
|
+
return {
|
|
102
|
+
appIdentifier: "acme-web",
|
|
103
|
+
allowedOrigins: ["https://app.acme.com"],
|
|
104
|
+
policy: {
|
|
105
|
+
isProjectEnabled: true,
|
|
106
|
+
isApplicationEnabled: true,
|
|
107
|
+
captureTrigger: "Always",
|
|
108
|
+
samplePercentage: 100,
|
|
109
|
+
consentMode: "NotRequired",
|
|
110
|
+
maskingMode: "MaskSensitiveInputsOnly",
|
|
111
|
+
retentionInDays: 7,
|
|
112
|
+
},
|
|
113
|
+
publishedRecorderVersion: "1.4.0",
|
|
114
|
+
lastConfigFetchAt: iso(12 * 1000),
|
|
115
|
+
lastChunkReceivedAt: iso(12 * 1000),
|
|
116
|
+
lastSessionStartedAt: iso(MINUTE_MS),
|
|
117
|
+
budgetExceededAt: null,
|
|
118
|
+
sessionsLast24h: 143,
|
|
119
|
+
playableSessionsLast24h: 120,
|
|
120
|
+
refusalsLast24h: [],
|
|
121
|
+
projectBytesUsedToday: 10 * MB,
|
|
122
|
+
dailyByteLimit: GB,
|
|
123
|
+
applicationBytesUsedThisMonth: 256 * MB,
|
|
124
|
+
monthlyBudgetInGB: 1,
|
|
125
|
+
...overrides,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function makeSnapshot(
|
|
130
|
+
status: RecordingHealthStatus,
|
|
131
|
+
overrides?: Partial<SessionReplayHealthSnapshot>,
|
|
132
|
+
): SessionReplayHealthSnapshot {
|
|
133
|
+
return {
|
|
134
|
+
status: status,
|
|
135
|
+
diagnosis: diagnoseRecordingHealth(status, NOW),
|
|
136
|
+
extras: {
|
|
137
|
+
dropsLast24h: [{ reason: "scrub-incomplete", count: 2 }],
|
|
138
|
+
recorderCapabilities: ["click-events"],
|
|
139
|
+
},
|
|
140
|
+
isLoading: false,
|
|
141
|
+
isRefreshing: false,
|
|
142
|
+
error: null,
|
|
143
|
+
fetchedAtUnixMs: NOW,
|
|
144
|
+
nowUnixMs: NOW,
|
|
145
|
+
...overrides,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderView(status: RecordingHealthStatus = makeStatus()): void {
|
|
150
|
+
render(
|
|
151
|
+
<MemoryRouter>
|
|
152
|
+
<RecordingHealthDashboardView
|
|
153
|
+
rumApplicationId={APP_ID}
|
|
154
|
+
health={makeSnapshot(status)}
|
|
155
|
+
/>
|
|
156
|
+
</MemoryRouter>,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function infoButton(label: string): HTMLElement {
|
|
161
|
+
const found: Array<HTMLElement> = screen.getAllByRole("button", {
|
|
162
|
+
name: `About ${label}`,
|
|
163
|
+
});
|
|
164
|
+
expect(found).toHaveLength(1);
|
|
165
|
+
return found[0]!;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function allInfoLabels(): Array<string> {
|
|
169
|
+
return screen
|
|
170
|
+
.queryAllByRole("button", { name: INFO_BUTTON_NAME })
|
|
171
|
+
.map((button: HTMLElement): string => {
|
|
172
|
+
return (button.getAttribute("aria-label") || "").replace("About ", "");
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function tooltipTextOf(button: HTMLElement): Promise<string> {
|
|
177
|
+
fireEvent.mouseEnter(button);
|
|
178
|
+
await act(async () => {
|
|
179
|
+
jest.advanceTimersByTime(200);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const describedBy: string | null = button.getAttribute("aria-describedby");
|
|
183
|
+
expect(describedBy).toBeTruthy();
|
|
184
|
+
|
|
185
|
+
const text: string =
|
|
186
|
+
document.getElementById(describedBy as string)?.textContent || "";
|
|
187
|
+
|
|
188
|
+
fireEvent.mouseLeave(button);
|
|
189
|
+
|
|
190
|
+
return text;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function expectExplained(
|
|
194
|
+
label: string,
|
|
195
|
+
key: RumRecordingHealthMetric,
|
|
196
|
+
): Promise<void> {
|
|
197
|
+
const button: HTMLElement = infoButton(label);
|
|
198
|
+
|
|
199
|
+
expect(button.parentElement?.closest("button, a")).toBeNull();
|
|
200
|
+
expect(await tooltipTextOf(button)).toBe(
|
|
201
|
+
RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS[key],
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function tone(testId: string): string | null {
|
|
206
|
+
return screen.getByTestId(testId).getAttribute("data-tone");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
jest.useFakeTimers();
|
|
211
|
+
postMock.mockReset();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
afterEach(() => {
|
|
215
|
+
cleanup();
|
|
216
|
+
jest.useRealTimers();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("the Replay Health descriptions", () => {
|
|
220
|
+
test("read as short, finished, distinct sentences", () => {
|
|
221
|
+
expectReadableDescriptionRecord(
|
|
222
|
+
RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS,
|
|
223
|
+
"RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS",
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("every pipeline stage has its own text", () => {
|
|
228
|
+
expect(PIPELINE_STAGE_TOOLTIPS).toEqual({
|
|
229
|
+
recorder: RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.recorderLoaded,
|
|
230
|
+
policy: RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.recordingAllowed,
|
|
231
|
+
uploads: RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.chunksReceived,
|
|
232
|
+
sessions: RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.sessionsLast24h,
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe("Replay Health tooltips", () => {
|
|
238
|
+
test("the stages, counters and meters explain themselves; settings and versions do not", async () => {
|
|
239
|
+
renderView();
|
|
240
|
+
|
|
241
|
+
expect(allInfoLabels()).toEqual([
|
|
242
|
+
"Recorder loaded",
|
|
243
|
+
"Recording allowed",
|
|
244
|
+
"Chunks received",
|
|
245
|
+
"Sessions in 24h",
|
|
246
|
+
"Refused at the gate",
|
|
247
|
+
"Dropped after acceptance",
|
|
248
|
+
"Project bytes today",
|
|
249
|
+
"This application this month",
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
await expectExplained("Recorder loaded", "recorderLoaded");
|
|
253
|
+
await expectExplained("Recording allowed", "recordingAllowed");
|
|
254
|
+
await expectExplained("Chunks received", "chunksReceived");
|
|
255
|
+
await expectExplained("Sessions in 24h", "sessionsLast24h");
|
|
256
|
+
await expectExplained("Refused at the gate", "refusedAtGate");
|
|
257
|
+
await expectExplained("Dropped after acceptance", "droppedAfterAcceptance");
|
|
258
|
+
await expectExplained("Project bytes today", "projectBytesToday");
|
|
259
|
+
await expectExplained(
|
|
260
|
+
"This application this month",
|
|
261
|
+
"applicationBytesThisMonth",
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
expect(
|
|
265
|
+
within(screen.getByTestId("health-policy")).queryAllByRole("button", {
|
|
266
|
+
name: INFO_BUTTON_NAME,
|
|
267
|
+
}),
|
|
268
|
+
).toHaveLength(0);
|
|
269
|
+
expect(
|
|
270
|
+
within(screen.getByTestId("health-recorder")).queryAllByRole("button", {
|
|
271
|
+
name: INFO_BUTTON_NAME,
|
|
272
|
+
}),
|
|
273
|
+
).toHaveLength(0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("each (i) sits in its own stage, list or meter", () => {
|
|
277
|
+
renderView();
|
|
278
|
+
|
|
279
|
+
for (const key of ["recorder", "policy", "uploads", "sessions"]) {
|
|
280
|
+
const stage: HTMLElement = screen.getByTestId(`health-stage-${key}`);
|
|
281
|
+
|
|
282
|
+
expect(
|
|
283
|
+
within(stage).getByTestId(`health-stage-${key}-info`),
|
|
284
|
+
).toBeInTheDocument();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
for (const testId of [
|
|
288
|
+
"health-refusals",
|
|
289
|
+
"health-drops",
|
|
290
|
+
"health-meter-project-day",
|
|
291
|
+
"health-meter-app-month",
|
|
292
|
+
]) {
|
|
293
|
+
expect(
|
|
294
|
+
within(screen.getByTestId(testId)).getByTestId(`${testId}-info`),
|
|
295
|
+
).toBeInTheDocument();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("an unknown counter keeps its (i): the explanation does not depend on the value", () => {
|
|
300
|
+
renderView(
|
|
301
|
+
makeStatus({
|
|
302
|
+
refusalsLast24h: null,
|
|
303
|
+
projectBytesUsedToday: null,
|
|
304
|
+
applicationBytesUsedThisMonth: null,
|
|
305
|
+
monthlyBudgetInGB: null,
|
|
306
|
+
sessionsLast24h: null,
|
|
307
|
+
playableSessionsLast24h: null,
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(allInfoLabels()).toHaveLength(8);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("the policy page's summary card shows the diagnosis only, with no (i)", () => {
|
|
315
|
+
render(
|
|
316
|
+
<MemoryRouter>
|
|
317
|
+
<RecordingHealthSummaryView
|
|
318
|
+
rumApplicationId={APP_ID}
|
|
319
|
+
health={makeSnapshot(makeStatus())}
|
|
320
|
+
/>
|
|
321
|
+
</MemoryRouter>,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
expect(screen.getByTestId("health-card")).toBeInTheDocument();
|
|
325
|
+
expect(allInfoLabels()).toEqual([]);
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("the thresholds the texts name are the ones the page colours by", () => {
|
|
330
|
+
test("Recorder loaded turns amber only past 24 hours", () => {
|
|
331
|
+
expect(SESSION_REPLAY_RECORDER_ACTIVE_WINDOW_MS).toBe(24 * HOUR_MS);
|
|
332
|
+
expect(RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.recorderLoaded).toContain(
|
|
333
|
+
"Amber after 24 hours",
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
renderView(makeStatus({ lastConfigFetchAt: iso(23 * HOUR_MS) }));
|
|
337
|
+
expect(tone("health-stage-recorder")).toBe("ok");
|
|
338
|
+
cleanup();
|
|
339
|
+
|
|
340
|
+
renderView(
|
|
341
|
+
makeStatus({ lastConfigFetchAt: iso(24 * HOUR_MS + MINUTE_MS) }),
|
|
342
|
+
);
|
|
343
|
+
expect(tone("health-stage-recorder")).toBe("warning");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("Chunks received turns amber only past 6 hours", () => {
|
|
347
|
+
expect(SESSION_REPLAY_STALE_CHUNK_MS).toBe(6 * HOUR_MS);
|
|
348
|
+
expect(RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.chunksReceived).toContain(
|
|
349
|
+
"amber after 6 hours without one",
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
renderView(makeStatus({ lastChunkReceivedAt: iso(5 * HOUR_MS) }));
|
|
353
|
+
expect(tone("health-stage-uploads")).toBe("ok");
|
|
354
|
+
cleanup();
|
|
355
|
+
|
|
356
|
+
renderView(
|
|
357
|
+
makeStatus({ lastChunkReceivedAt: iso(6 * HOUR_MS + MINUTE_MS) }),
|
|
358
|
+
);
|
|
359
|
+
expect(tone("health-stage-uploads")).toBe("warning");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("Chunks received shows every refused request once one reason reaches 5", () => {
|
|
363
|
+
expect(SESSION_REPLAY_REFUSAL_ALERT_THRESHOLD).toBe(5);
|
|
364
|
+
expect(RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.chunksReceived).toContain(
|
|
365
|
+
"When one refusal reason has 5 or more below",
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
renderView(
|
|
369
|
+
makeStatus({
|
|
370
|
+
refusalsLast24h: [
|
|
371
|
+
{ reason: "origin-not-allowed", count: 4 },
|
|
372
|
+
{ reason: "not-sampled", count: 4 },
|
|
373
|
+
],
|
|
374
|
+
}),
|
|
375
|
+
);
|
|
376
|
+
// Eight refusals, but no single reason reached the threshold.
|
|
377
|
+
expect(screen.getByTestId("health-stage-uploads-value")).toHaveTextContent(
|
|
378
|
+
"12s ago",
|
|
379
|
+
);
|
|
380
|
+
cleanup();
|
|
381
|
+
|
|
382
|
+
renderView(
|
|
383
|
+
makeStatus({
|
|
384
|
+
refusalsLast24h: [
|
|
385
|
+
{ reason: "origin-not-allowed", count: 5 },
|
|
386
|
+
{ reason: "not-sampled", count: 3 },
|
|
387
|
+
],
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
// The value is the total across reasons, not the top reason alone.
|
|
391
|
+
expect(screen.getByTestId("health-stage-uploads-value")).toHaveTextContent(
|
|
392
|
+
"8 refused",
|
|
393
|
+
);
|
|
394
|
+
expect(screen.getByTestId("health-refusals-total")).toHaveTextContent(
|
|
395
|
+
"8 total",
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("a byte meter turns amber from 80% and red at 100%", () => {
|
|
400
|
+
expect(USAGE_WARNING_PERCENT).toBe(80);
|
|
401
|
+
expect(
|
|
402
|
+
RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.projectBytesToday,
|
|
403
|
+
).toContain("amber from 80%");
|
|
404
|
+
expect(
|
|
405
|
+
RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.applicationBytesThisMonth,
|
|
406
|
+
).toContain("amber from 80%");
|
|
407
|
+
|
|
408
|
+
renderView(
|
|
409
|
+
makeStatus({
|
|
410
|
+
projectBytesUsedToday: Math.floor(GB * 0.79),
|
|
411
|
+
applicationBytesUsedThisMonth: Math.ceil(GB * 0.8),
|
|
412
|
+
}),
|
|
413
|
+
);
|
|
414
|
+
expect(tone("health-meter-project-day")).toBe("ok");
|
|
415
|
+
expect(tone("health-meter-app-month")).toBe("warning");
|
|
416
|
+
cleanup();
|
|
417
|
+
|
|
418
|
+
renderView(
|
|
419
|
+
makeStatus({
|
|
420
|
+
projectBytesUsedToday: GB,
|
|
421
|
+
applicationBytesUsedThisMonth: GB,
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
expect(tone("health-meter-project-day")).toBe("error");
|
|
425
|
+
expect(tone("health-meter-app-month")).toBe("error");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
/*
|
|
429
|
+
* Which sessions the server calls playable is pinned against its SQL in
|
|
430
|
+
* App/Tests/Dashboard/RumFlowsUsersHealthTooltipWiring.test.ts; here, the
|
|
431
|
+
* stage shows the two numbers the text talks about.
|
|
432
|
+
*/
|
|
433
|
+
test("Sessions in 24h shows the count, with the playable figure beside it", () => {
|
|
434
|
+
expect(RUM_RECORDING_HEALTH_METRIC_DESCRIPTIONS.sessionsLast24h).toContain(
|
|
435
|
+
"sessions still recording count as playable",
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
renderView(makeStatus({ sessionsLast24h: 10, playableSessionsLast24h: 7 }));
|
|
439
|
+
|
|
440
|
+
expect(screen.getByTestId("health-stage-sessions-value")).toHaveTextContent(
|
|
441
|
+
"10",
|
|
442
|
+
);
|
|
443
|
+
expect(screen.getByTestId("health-stage-sessions")).toHaveTextContent(
|
|
444
|
+
"7 playable",
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
});
|