@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.
Files changed (166) hide show
  1. package/Models/DatabaseModels/AlertFeed.ts +2 -0
  2. package/Models/DatabaseModels/IncidentAlert.ts +540 -0
  3. package/Models/DatabaseModels/IncidentFeed.ts +2 -0
  4. package/Models/DatabaseModels/Index.ts +2 -0
  5. package/Models/DatabaseModels/Project.ts +60 -0
  6. package/Models/DatabaseModels/RumApplication.ts +52 -1
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  10. package/Server/Services/IncidentAlertService.ts +1811 -0
  11. package/Server/Services/IncidentService.ts +210 -10
  12. package/Server/Services/IncidentStateTimelineService.ts +24 -0
  13. package/Server/Services/Index.ts +2 -0
  14. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
  15. package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
  16. package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
  17. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
  18. package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
  19. package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
  20. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
  21. package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
  22. package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
  23. package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
  24. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
  25. package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
  26. package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
  27. package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
  28. package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
  29. package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
  30. package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
  31. package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
  32. package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
  33. package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
  34. package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
  35. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
  36. package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
  37. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
  38. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
  39. package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
  40. package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
  41. package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
  42. package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
  43. package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
  44. package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
  45. package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
  46. package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
  47. package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
  48. package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
  49. package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
  50. package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
  51. package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
  52. package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
  53. package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
  54. package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
  55. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
  56. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
  57. package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
  58. package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
  59. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
  60. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
  61. package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
  62. package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
  63. package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
  64. package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
  65. package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
  66. package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
  67. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
  68. package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
  69. package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
  70. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
  71. package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
  72. package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
  73. package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
  74. package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
  75. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
  76. package/Tests/Types/Rum/WebVitals.test.ts +173 -0
  77. package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
  78. package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
  79. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
  80. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
  81. package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
  82. package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
  83. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
  84. package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
  85. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
  86. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
  87. package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
  88. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
  89. package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
  90. package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
  91. package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
  92. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
  93. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
  94. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
  95. package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
  96. package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
  97. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  98. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  99. package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
  100. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
  101. package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
  102. package/Types/Incident/IncidentAlertLink.ts +38 -0
  103. package/Types/Permission.ts +48 -0
  104. package/Types/Rum/SessionReplay.ts +25 -6
  105. package/Types/Rum/WebVitals.ts +52 -0
  106. package/UI/Components/Detail/FieldLabel.tsx +8 -0
  107. package/UI/Components/InfoCard/InfoCard.tsx +47 -1
  108. package/UI/Components/ModelTable/BaseModelTable.tsx +95 -17
  109. package/UI/Components/ModelTable/Column.ts +5 -0
  110. package/UI/Components/Table/TableHeader.tsx +47 -2
  111. package/UI/Components/Table/Types/Column.ts +7 -0
  112. package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
  113. package/UI/Utils/PermissionGate.ts +11 -2
  114. package/Utils/Incident/IncidentFromAlerts.ts +516 -0
  115. package/Utils/Rum/SessionTraceState.ts +212 -0
  116. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
  117. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  118. package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
  119. package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
  120. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
  121. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  122. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  123. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  125. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
  127. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
  130. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
  131. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
  132. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  133. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  134. package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
  135. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
  136. package/build/dist/Server/Services/IncidentService.js +164 -12
  137. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  138. package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
  139. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  140. package/build/dist/Server/Services/Index.js +2 -0
  141. package/build/dist/Server/Services/Index.js.map +1 -1
  142. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
  143. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  144. package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
  145. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
  146. package/build/dist/Types/Permission.js +42 -0
  147. package/build/dist/Types/Permission.js.map +1 -1
  148. package/build/dist/Types/Rum/WebVitals.js +25 -0
  149. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  150. package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
  151. package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
  152. package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
  153. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  154. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +66 -14
  155. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  156. package/build/dist/UI/Components/Table/TableHeader.js +18 -2
  157. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  158. package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
  159. package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
  160. package/build/dist/UI/Utils/PermissionGate.js +4 -2
  161. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  162. package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
  163. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
  164. package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
  165. package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
  166. package/package.json +1 -1
@@ -122,7 +122,9 @@ jest.mock("../../../UI/Utils/Permission", () => {
122
122
 
123
123
  import RumApplicationSessionReplaySettings, {
124
124
  EffectiveRecordingStatePill,
125
+ SAME_ORIGIN_TRACE_PROPAGATION_TITLE,
125
126
  describeEffectiveRecordingState,
127
+ describeSameOriginTracePropagation,
126
128
  } from "../../../../App/FeatureSet/Dashboard/src/Pages/Rum/View/SessionReplaySettings";
127
129
  import useSessionReplayHealth, {
128
130
  SESSION_REPLAY_INGEST_STATUS_ROUTE,
@@ -583,3 +585,150 @@ describe("EffectiveRecordingStatePill (standalone)", () => {
583
585
  expect(ingestStatusCalls()).toHaveLength(1);
584
586
  });
585
587
  });
588
+
589
+ /*
590
+ * RumApplication.sessionReplaySameOriginTracePropagation on the Replay
591
+ * Policy page: the read view names the switch and what it does, and the
592
+ * privacy summary gains its backend-link sentence exactly while it is on.
593
+ * The column defaults to true and the gate reads anything but an explicit
594
+ * false as on, so an unset value must read as on here too.
595
+ */
596
+ const SAME_ORIGIN_ON_COPY: string =
597
+ "On: requests to your own origin carry a traceparent and this session's id";
598
+ const SAME_ORIGIN_OFF_COPY: string =
599
+ "Off: nothing is added to requests to your own origin";
600
+
601
+ describe("describeSameOriginTracePropagation", () => {
602
+ it("only an explicit false reads as off", () => {
603
+ expect(describeSameOriginTracePropagation(false)).toBe(
604
+ SAME_ORIGIN_OFF_COPY,
605
+ );
606
+ expect(describeSameOriginTracePropagation(true)).toBe(SAME_ORIGIN_ON_COPY);
607
+ expect(describeSameOriginTracePropagation(undefined)).toBe(
608
+ SAME_ORIGIN_ON_COPY,
609
+ );
610
+ expect(describeSameOriginTracePropagation(null)).toBe(SAME_ORIGIN_ON_COPY);
611
+ });
612
+
613
+ it("carries the fixed setting title", () => {
614
+ expect(SAME_ORIGIN_TRACE_PROPAGATION_TITLE).toBe(
615
+ "Same-origin trace propagation",
616
+ );
617
+ });
618
+ });
619
+
620
+ describe("Replay Policy page: same-origin trace propagation", () => {
621
+ async function renderWithRow(
622
+ overrides: Partial<RumApplication>,
623
+ ): Promise<HealthControl> {
624
+ getItemMock.mockImplementation((): Promise<RumApplication> => {
625
+ return Promise.resolve(makeApplication(overrides));
626
+ });
627
+
628
+ const health: HealthControl = holdHealth();
629
+
630
+ renderPage();
631
+
632
+ await waitForPolicyRows();
633
+
634
+ return health;
635
+ }
636
+
637
+ it("the policy fetch selects the switch", async () => {
638
+ const health: HealthControl = await renderWithRow({});
639
+
640
+ const select: JSONObject =
641
+ ((policyGetItemCalls()[0]?.[0] as { select?: JSONObject })
642
+ .select as JSONObject) || {};
643
+
644
+ expect(select["sessionReplaySameOriginTracePropagation"]).toBe(true);
645
+ expect(select["sessionReplayTracePropagationOrigins"]).toBe(true);
646
+
647
+ await act(async (): Promise<void> => {
648
+ health.release(wireStatus());
649
+ });
650
+ });
651
+
652
+ it("a row with it on reads on and adds the backend-link sentence", async () => {
653
+ const health: HealthControl = await renderWithRow({
654
+ sessionReplaySameOriginTracePropagation: true,
655
+ sessionReplayCaptureUserIdentity: false,
656
+ });
657
+
658
+ const card: HTMLElement = getPolicyCard();
659
+
660
+ expect(
661
+ within(card).getByText(SAME_ORIGIN_TRACE_PROPAGATION_TITLE),
662
+ ).toBeInTheDocument();
663
+ expect(within(card).getByText(SAME_ORIGIN_ON_COPY)).toBeInTheDocument();
664
+ expect(
665
+ within(card).getByText(
666
+ "None: no cross-origin request gets a traceparent",
667
+ ),
668
+ ).toBeInTheDocument();
669
+
670
+ await waitFor(
671
+ () => {
672
+ expect(
673
+ screen.getByTestId("privacy-summary-backend-link"),
674
+ ).toHaveTextContent(
675
+ "Same-origin requests carry this session's id to your backend",
676
+ );
677
+ },
678
+ { timeout: WAIT_TIMEOUT },
679
+ );
680
+ expect(screen.getByTestId("privacy-summary-identity")).toHaveTextContent(
681
+ "can still name the person",
682
+ );
683
+
684
+ await act(async (): Promise<void> => {
685
+ health.release(wireStatus());
686
+ });
687
+ });
688
+
689
+ it("a row with it off reads off and has no backend-link sentence", async () => {
690
+ const health: HealthControl = await renderWithRow({
691
+ sessionReplaySameOriginTracePropagation: false,
692
+ sessionReplayCaptureUserIdentity: false,
693
+ });
694
+
695
+ expect(
696
+ within(getPolicyCard()).getByText(SAME_ORIGIN_OFF_COPY),
697
+ ).toBeInTheDocument();
698
+
699
+ await waitFor(
700
+ () => {
701
+ expect(
702
+ screen.getByTestId("privacy-summary-identity"),
703
+ ).toHaveTextContent("cannot be found by who the person was");
704
+ },
705
+ { timeout: WAIT_TIMEOUT },
706
+ );
707
+ expect(screen.queryByTestId("privacy-summary-backend-link")).toBeNull();
708
+
709
+ await act(async (): Promise<void> => {
710
+ health.release(wireStatus());
711
+ });
712
+ });
713
+
714
+ it("a row that never set it reads as the on default, in both places", async () => {
715
+ const health: HealthControl = await renderWithRow({});
716
+
717
+ expect(
718
+ within(getPolicyCard()).getByText(SAME_ORIGIN_ON_COPY),
719
+ ).toBeInTheDocument();
720
+
721
+ await waitFor(
722
+ () => {
723
+ expect(
724
+ screen.getByTestId("privacy-summary-backend-link"),
725
+ ).toHaveAttribute("data-default", "true");
726
+ },
727
+ { timeout: WAIT_TIMEOUT },
728
+ );
729
+
730
+ await act(async (): Promise<void> => {
731
+ health.release(wireStatus());
732
+ });
733
+ });
734
+ });
@@ -17,9 +17,10 @@ import { RecordingHealthStatus } from "../../../Types/Rum/SessionReplayHealth";
17
17
  * The three live checks flip as the health status changes; "Watch it"
18
18
  * appears only with a playable session and opens the newest one through
19
19
  * the LIST endpoint (never the manifest, which writes an audit row); the
20
- * not-sampled cause quotes the current sample percentage; the
21
- * onSessionChange snippet is present; the install snippet uses the safe
22
- * identifier or the placeholder; the CSP block keeps 'self'
20
+ * not-sampled cause quotes the current sample percentage; step 4 says
21
+ * backend linking is automatic, names the cross-origin step and offers
22
+ * the onSessionChange snippet as optional (issue #3979); the install
23
+ * snippet uses the safe identifier or the placeholder; the CSP block keeps 'self'
23
24
  * (session-list-15); "Run the installation test" lands on THIS
24
25
  * application's settings (settings-setup-17).
25
26
  */
@@ -61,6 +62,7 @@ import SessionReplaySetupGuide, {
61
62
  import { clearSessionReplayHealthStore } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/useSessionReplayHealth";
62
63
  import {
63
64
  buildCspSnippet,
65
+ buildOnSessionChangeSnippet,
64
66
  buildScriptTagSnippet,
65
67
  getSafeAppIdentifier,
66
68
  } from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/SessionReplayInstallSnippet";
@@ -378,6 +380,74 @@ describe("Install snippet builders", () => {
378
380
  expect(csp).toContain("script-src 'self' https://one.example.com;");
379
381
  expect(csp).toContain("connect-src 'self' https://one.example.com;");
380
382
  });
383
+
384
+ /*
385
+ * Issue #3979: the session hook is no longer the join key for backend
386
+ * telemetry - the recorder's same-origin tracestate is - so the snippet
387
+ * is labelled optional and only stamps the page's own browser spans.
388
+ * It stamps each span at start through a span processor instead of
389
+ * mutating resource.attributes, which the exporter reads at export time
390
+ * and so re-labels spans batched before a rotation.
391
+ */
392
+ it("the correlation snippet is optional and stamps the page's own spans at start", () => {
393
+ const snippet: string = buildOnSessionChangeSnippet();
394
+
395
+ expect(snippet).toMatch(/^\/\/ Optional\./);
396
+ expect(snippet).toContain(
397
+ "Requests to your own origin are linked without it",
398
+ );
399
+ expect(snippet).toContain('"onSessionChange"');
400
+ expect(snippet).toContain("onStart: (span) => {");
401
+ expect(snippet).toContain(
402
+ 'span.setAttribute("session.id", replaySessionId)',
403
+ );
404
+ expect(snippet).toContain("forceFlush: () => Promise.resolve()");
405
+ expect(snippet).toContain("shutdown: () => Promise.resolve()");
406
+
407
+ /* The old manual path: a mutated resource and a server-side header. */
408
+ expect(snippet).not.toContain("resource.attributes");
409
+ expect(snippet).not.toContain("baggage");
410
+ expect(snippet).not.toContain("session.tab.id");
411
+ });
412
+
413
+ /*
414
+ * Index.onSessionChange tells the listener the id as soon as a recorder
415
+ * exists - before consent under Require explicit, before an on-error
416
+ * trigger, even for an unsampled session - and the processor then stamps
417
+ * every browser span from the first page load. The docs warn about it;
418
+ * the snippet people actually copy has to as well.
419
+ */
420
+ it("the correlation snippet carries the consent caveat", () => {
421
+ const snippet: string = buildOnSessionChangeSnippet();
422
+ const commentText: string = snippet
423
+ .split("\n")
424
+ .filter((line: string): boolean => {
425
+ return line.startsWith("// ");
426
+ })
427
+ .map((line: string): string => {
428
+ return line.slice(3);
429
+ })
430
+ .join(" ");
431
+
432
+ expect(commentText).toContain(
433
+ "Fires as soon as the recorder starts - before consent or a trigger - so gate on your own consent state if that matters.",
434
+ );
435
+ /* A comment above the code, so it cannot change what the snippet does. */
436
+ expect(snippet.indexOf("gate on your own consent state")).toBeLessThan(
437
+ snippet.indexOf("let replaySessionId = null;"),
438
+ );
439
+ expect(snippet).toMatch(/^\/\/ Optional\./);
440
+ });
441
+
442
+ it("the correlation snippet queues the listener, so it works before the recorder loads", () => {
443
+ const snippet: string = buildOnSessionChangeSnippet();
444
+
445
+ expect(snippet).toContain(
446
+ "(window.OneUptimeReplayQueue = window.OneUptimeReplayQueue || []).push([",
447
+ );
448
+ /* No direct call on a global that may not exist yet. */
449
+ expect(snippet).not.toContain("OneUptimeReplay.onSessionChange(");
450
+ });
381
451
  });
382
452
 
383
453
  describe("SessionReplaySetupGuide (rendered)", () => {
@@ -406,13 +476,44 @@ describe("SessionReplaySetupGuide (rendered)", () => {
406
476
  expect(screen.getByTestId("setup-step-2")).toHaveTextContent(
407
477
  'data-oneuptime-app-identifier="acme-web"',
408
478
  );
409
- /* Step 4 carries the correlation hook. */
479
+ /*
480
+ * Step 4: backend linking is automatic (issue #3979), the one manual
481
+ * step left is for an API on another origin, and the session hook is
482
+ * an optional extra for the page's own browser spans.
483
+ */
484
+ expect(screen.getByTestId("setup-correlation-automatic")).toHaveTextContent(
485
+ "Nothing to install",
486
+ );
487
+ expect(screen.getByTestId("setup-correlation-automatic")).toHaveTextContent(
488
+ "own origin",
489
+ );
490
+ expect(screen.getByTestId("setup-correlation-automatic")).toHaveTextContent(
491
+ "tracestate",
492
+ );
493
+ expect(
494
+ screen.getByTestId("setup-correlation-cross-origin"),
495
+ ).toHaveTextContent("Trace propagation origins");
496
+ expect(
497
+ screen.getByTestId("setup-correlation-cross-origin"),
498
+ ).toHaveTextContent("Access-Control-Allow-Headers");
499
+ expect(
500
+ screen.getByTestId("setup-correlation-cross-origin"),
501
+ ).toHaveTextContent("Same-origin trace propagation");
410
502
  expect(screen.getByTestId("setup-correlation-snippet")).toHaveTextContent(
411
- "OneUptimeReplay.onSessionChange",
503
+ "onSessionChange",
412
504
  );
413
505
  expect(screen.getByTestId("setup-correlation-snippet")).toHaveTextContent(
414
- 'resource.attributes["session.id"]',
506
+ 'span.setAttribute("session.id"',
415
507
  );
508
+ expect(
509
+ screen.getByTestId("setup-correlation-snippet"),
510
+ ).not.toHaveTextContent("resource.attributes");
511
+ /* Step 4 shows the optional snippet only, not the script tag again. */
512
+ expect(
513
+ screen.getByTestId("setup-correlation-snippet"),
514
+ ).not.toHaveTextContent("data-oneuptime-token");
515
+ expect(screen.getAllByTestId("install-snippet")).toHaveLength(1);
516
+ expect(screen.getByTestId("setup-step-4")).not.toHaveTextContent("baggage");
416
517
  /* Step 3 keeps 'self'. */
417
518
  expect(screen.getByTestId("setup-csp-snippet")).toHaveTextContent(
418
519
  "script-src 'self'",
@@ -0,0 +1,330 @@
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 HTTPResponse from "../../../Types/API/HTTPResponse";
22
+ import { JSONObject } from "../../../Types/JSON";
23
+ import TimeRange from "../../../Types/Time/TimeRange";
24
+ import getJestMockFunction, { MockFunction } from "../../MockType";
25
+
26
+ /*
27
+ * The Session Replay Users table's (i) tooltips: Sessions, Last seen, Time
28
+ * and Signals explain themselves in their headers; User and Actions do not.
29
+ *
30
+ * Each (i) is hovered and must show its own text from
31
+ * RUM_REPLAY_USERS_METRIC_DESCRIPTIONS, it sits in the header - never in a
32
+ * row, whose click opens the person's sessions - and the cells under each
33
+ * header read the rollup fields the text describes.
34
+ */
35
+
36
+ const postMock: MockFunction = getJestMockFunction();
37
+
38
+ jest.mock("../../../UI/Utils/API/API", () => {
39
+ return {
40
+ __esModule: true,
41
+ default: {
42
+ post: (...args: Array<unknown>) => {
43
+ return postMock(...args);
44
+ },
45
+ getFriendlyMessage: (error: unknown): string => {
46
+ return error instanceof HTTPErrorResponse
47
+ ? error.message
48
+ : String(error);
49
+ },
50
+ },
51
+ };
52
+ });
53
+
54
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
55
+ return {
56
+ __esModule: true,
57
+ default: {
58
+ getCommonHeaders: (): Record<string, string> => {
59
+ return {};
60
+ },
61
+ },
62
+ };
63
+ });
64
+
65
+ import SessionReplayUsersTable from "../../../../App/FeatureSet/Dashboard/src/Components/SessionReplay/SessionReplayUsersTable";
66
+ import {
67
+ RUM_REPLAY_USERS_METRIC_DESCRIPTIONS,
68
+ RumReplayUsersMetric,
69
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/MetricDescriptions/RumMetricDescriptions";
70
+ import { expectReadableDescriptionRecord } from "../../App/Dashboard/MetricDescriptionRules";
71
+
72
+ const APP_ID: string = "0193c0de-1111-4aaa-8bbb-000000000001";
73
+ const USER_KEY: string =
74
+ "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08";
75
+ const HOUR_MS: number = 60 * 60 * 1000;
76
+ // Every (i) is a button named "About <what it explains>".
77
+ const INFO_BUTTON_NAME: RegExp = /^About /;
78
+
79
+ function wireRollup(overrides?: JSONObject): JSONObject {
80
+ const now: number = Date.now();
81
+
82
+ return {
83
+ groupKey: `u:${USER_KEY}`,
84
+ kind: "identified",
85
+ identifiedUserKey: USER_KEY,
86
+ visitorId: "7f3a2b1c9d8e4f5a6b7c8d9e0f1a2b3c",
87
+ identifiedUserLabel: "jane@acme.com",
88
+ identifiedUserTraits: {},
89
+ sessionCount: 3,
90
+ liveSessionCount: 0,
91
+ firstSeenUnixMs: now - 5 * HOUR_MS,
92
+ lastSeenUnixMs: now - 2 * HOUR_MS,
93
+ totalDurationMs: 45 * 60 * 1000,
94
+ errorCount: 4,
95
+ frustrationCount: 2,
96
+ errorSessionCount: 1,
97
+ pageCount: 0,
98
+ lastSessionId: "a1b2c3d4e5f60718293a4b5c6d7e8f90",
99
+ lastEntryUrl: "https://app.acme.com/checkout",
100
+ browserName: "Chrome",
101
+ browserVersion: "126",
102
+ osName: "macOS",
103
+ deviceType: "desktop",
104
+ countryCode: "DE",
105
+ ...overrides,
106
+ };
107
+ }
108
+
109
+ async function flush(): Promise<void> {
110
+ for (let i: number = 0; i < 5; i++) {
111
+ await act(async () => {
112
+ await Promise.resolve();
113
+ });
114
+ }
115
+ }
116
+
117
+ async function renderTable(users: Array<JSONObject>): Promise<void> {
118
+ postMock.mockImplementation((): Promise<HTTPResponse<JSONObject>> => {
119
+ return Promise.resolve(
120
+ new HTTPResponse<JSONObject>(200, { users, nextCursor: null }, {}),
121
+ );
122
+ });
123
+
124
+ render(
125
+ <MemoryRouter>
126
+ <SessionReplayUsersTable
127
+ rumApplicationId={APP_ID}
128
+ timeRange={{ range: TimeRange.PAST_ONE_DAY }}
129
+ reloadToken={0}
130
+ onViewUserSessions={onViewUserSessions}
131
+ />
132
+ </MemoryRouter>,
133
+ );
134
+
135
+ await flush();
136
+ }
137
+
138
+ let onViewUserSessions: MockFunction = getJestMockFunction();
139
+
140
+ function infoButton(title: string): HTMLElement {
141
+ const found: Array<HTMLElement> = screen.getAllByRole("button", {
142
+ name: `About ${title}`,
143
+ });
144
+ expect(found).toHaveLength(1);
145
+ return found[0]!;
146
+ }
147
+
148
+ function allInfoLabels(): Array<string> {
149
+ return screen
150
+ .queryAllByRole("button", { name: INFO_BUTTON_NAME })
151
+ .map((button: HTMLElement): string => {
152
+ return (button.getAttribute("aria-label") || "").replace("About ", "");
153
+ });
154
+ }
155
+
156
+ async function tooltipTextOf(button: HTMLElement): Promise<string> {
157
+ fireEvent.mouseEnter(button);
158
+ await act(async () => {
159
+ jest.advanceTimersByTime(200);
160
+ });
161
+
162
+ const describedBy: string | null = button.getAttribute("aria-describedby");
163
+ expect(describedBy).toBeTruthy();
164
+
165
+ const text: string =
166
+ document.getElementById(describedBy as string)?.textContent || "";
167
+
168
+ fireEvent.mouseLeave(button);
169
+
170
+ return text;
171
+ }
172
+
173
+ async function expectExplained(
174
+ title: string,
175
+ key: RumReplayUsersMetric,
176
+ ): Promise<void> {
177
+ const button: HTMLElement = infoButton(title);
178
+
179
+ expect(button.closest("thead")).not.toBeNull();
180
+ expect(button.parentElement?.closest("button, a")).toBeNull();
181
+ expect(await tooltipTextOf(button)).toBe(
182
+ RUM_REPLAY_USERS_METRIC_DESCRIPTIONS[key],
183
+ );
184
+ }
185
+
186
+ /* The row's cells, in column order: User, Sessions, Last seen, Time, Signals, Actions. */
187
+ function rowCells(): Array<HTMLElement> {
188
+ const [row] = screen.getAllByTestId("session-user-row");
189
+
190
+ return Array.from(row!.querySelectorAll("td"));
191
+ }
192
+
193
+ beforeEach(() => {
194
+ jest.useFakeTimers();
195
+ postMock.mockReset();
196
+ onViewUserSessions = getJestMockFunction();
197
+ });
198
+
199
+ afterEach(() => {
200
+ cleanup();
201
+ jest.useRealTimers();
202
+ });
203
+
204
+ describe("the Session Replay Users descriptions", () => {
205
+ test("read as short, finished, distinct sentences", () => {
206
+ expectReadableDescriptionRecord(
207
+ RUM_REPLAY_USERS_METRIC_DESCRIPTIONS,
208
+ "RUM_REPLAY_USERS_METRIC_DESCRIPTIONS",
209
+ );
210
+ });
211
+ });
212
+
213
+ describe("SessionReplayUsersTable header tooltips", () => {
214
+ test("Sessions, Last seen, Time and Signals explain themselves; User and Actions do not", async () => {
215
+ await renderTable([wireRollup()]);
216
+
217
+ expect(screen.getAllByTestId("session-user-row")).toHaveLength(1);
218
+ expect(allInfoLabels()).toEqual([
219
+ "Sessions",
220
+ "Last seen",
221
+ "Time",
222
+ "Signals",
223
+ ]);
224
+
225
+ await expectExplained("Sessions", "sessions");
226
+ await expectExplained("Last seen", "lastSeen");
227
+ await expectExplained("Time", "time");
228
+ await expectExplained("Signals", "signals");
229
+ });
230
+
231
+ test("the headers carry their (i) even when the range is empty", async () => {
232
+ await renderTable([]);
233
+
234
+ expect(screen.getByTestId("session-users-empty")).toBeInTheDocument();
235
+ expect(allInfoLabels()).toEqual([
236
+ "Sessions",
237
+ "Last seen",
238
+ "Time",
239
+ "Signals",
240
+ ]);
241
+ });
242
+
243
+ test("clicking or pressing Enter on an (i) never opens a person's sessions", async () => {
244
+ await renderTable([wireRollup()]);
245
+
246
+ const info: HTMLElement = infoButton("Signals");
247
+
248
+ fireEvent.click(info);
249
+ fireEvent.keyDown(info, { key: "Enter" });
250
+
251
+ expect(onViewUserSessions).not.toHaveBeenCalled();
252
+
253
+ // The row itself still does.
254
+ fireEvent.click(screen.getByTestId("session-user-row"));
255
+
256
+ expect(onViewUserSessions).toHaveBeenCalledTimes(1);
257
+ });
258
+ });
259
+
260
+ describe("SessionReplayUsersTable cells read what the header texts describe", () => {
261
+ test("Sessions shows the count, then live sessions or the earliest start in the range", async () => {
262
+ await renderTable([
263
+ wireRollup(),
264
+ wireRollup({
265
+ groupKey: "v:visitor-2",
266
+ kind: "visitor",
267
+ identifiedUserKey: "",
268
+ identifiedUserLabel: "",
269
+ visitorId: "visitor-2",
270
+ sessionCount: 2,
271
+ liveSessionCount: 1,
272
+ }),
273
+ ]);
274
+
275
+ const rows: Array<HTMLElement> = screen.getAllByTestId("session-user-row");
276
+ const [, firstSessions] = Array.from(rows[0]!.querySelectorAll("td"));
277
+ const [, secondSessions] = Array.from(rows[1]!.querySelectorAll("td"));
278
+
279
+ expect(firstSessions).toHaveTextContent("3");
280
+ // firstSeenUnixMs is the earliest session IN THE RANGE, five hours ago.
281
+ expect(firstSessions).toHaveTextContent("first seen 5 hours ago");
282
+ expect(secondSessions).toHaveTextContent("2");
283
+ expect(secondSessions).toHaveTextContent("1 live");
284
+ expect(RUM_REPLAY_USERS_METRIC_DESCRIPTIONS.sessions).toContain(
285
+ "not necessarily their first visit ever",
286
+ );
287
+ });
288
+
289
+ test("Last seen is the newest session's start, however long that session has run", async () => {
290
+ await renderTable([wireRollup()]);
291
+
292
+ const lastSeen: HTMLElement = within(rowCells()[2]!).getByTestId(
293
+ "session-user-last-seen",
294
+ );
295
+
296
+ expect(lastSeen).toHaveTextContent("2 hours ago");
297
+ expect(RUM_REPLAY_USERS_METRIC_DESCRIPTIONS.lastSeen).toContain(
298
+ "newest session in the selected range started",
299
+ );
300
+ });
301
+
302
+ test("Time adds the durations up; its page line is the route-change count, which can be 0", async () => {
303
+ await renderTable([wireRollup()]);
304
+
305
+ const time: HTMLElement = rowCells()[3]!;
306
+
307
+ expect(time).toHaveTextContent("45m");
308
+ /*
309
+ * pageCount is the recorder's route-change counter, so a person who
310
+ * only ever loaded pages in full reads "0 pages" - which is why the
311
+ * text says a page opened by a full load is not counted.
312
+ */
313
+ expect(time).toHaveTextContent("0 pages");
314
+ expect(RUM_REPLAY_USERS_METRIC_DESCRIPTIONS.time).toContain(
315
+ "a page opened by a full load is not counted",
316
+ );
317
+ });
318
+
319
+ test("Signals shows the summed error and frustration counts, with the error sessions on hover", async () => {
320
+ await renderTable([wireRollup()]);
321
+
322
+ const signals: HTMLElement = rowCells()[4]!;
323
+
324
+ expect(signals).toHaveTextContent("4 errors");
325
+ expect(signals).toHaveTextContent("2 frustration");
326
+ expect(
327
+ within(signals).getByText("4 errors").closest("[title]"),
328
+ ).toHaveAttribute("title", "in 1 session");
329
+ });
330
+ });