@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
@@ -0,0 +1,239 @@
1
+ import {
2
+ ParsedSessionTraceState,
3
+ SESSION_TRACE_STATE_KEY,
4
+ buildSessionTraceStateMember,
5
+ parseSessionTraceState,
6
+ } from "../../../Utils/Rum/SessionTraceState";
7
+
8
+ const SESSION_ID: string = "0123456789abcdef0123456789abcdef";
9
+ const PARENT_ID: string = "fedcba9876543210";
10
+
11
+ /*
12
+ * The W3C / OpenTelemetry validation rules for a tracestate member, copied
13
+ * from @opentelemetry/core's validators. A member that fails them is
14
+ * dropped by the backend's propagator before any span sees it.
15
+ */
16
+ const W3C_KEY_PATTERN: RegExp =
17
+ /^(?:[a-z][_0-9a-z\-*/]{0,255}|[a-z0-9][_0-9a-z\-*/]{0,240}@[a-z][_0-9a-z\-*/]{0,13})$/;
18
+ const W3C_VALUE_PATTERN: RegExp = /^[ -~]{0,255}[!-~]$/;
19
+ const W3C_VALUE_FORBIDDEN: RegExp = /,|=/;
20
+
21
+ describe("SessionTraceState", () => {
22
+ describe("buildSessionTraceStateMember", () => {
23
+ it("writes the session id under the oneuptime key", () => {
24
+ expect(buildSessionTraceStateMember(SESSION_ID)).toBe(
25
+ `oneuptime=sid:${SESSION_ID}`,
26
+ );
27
+ expect(SESSION_TRACE_STATE_KEY).toBe("oneuptime");
28
+ });
29
+
30
+ it("adds the synthetic parent id only when one is given", () => {
31
+ expect(buildSessionTraceStateMember(SESSION_ID, PARENT_ID)).toBe(
32
+ `oneuptime=sid:${SESSION_ID};p:${PARENT_ID}`,
33
+ );
34
+ expect(buildSessionTraceStateMember(SESSION_ID, null)).toBe(
35
+ `oneuptime=sid:${SESSION_ID}`,
36
+ );
37
+ expect(buildSessionTraceStateMember(SESSION_ID, "")).toBe(
38
+ `oneuptime=sid:${SESSION_ID}`,
39
+ );
40
+ });
41
+
42
+ it("produces a member every W3C tracestate parser accepts", () => {
43
+ for (const member of [
44
+ buildSessionTraceStateMember(SESSION_ID),
45
+ buildSessionTraceStateMember(SESSION_ID, PARENT_ID),
46
+ ]) {
47
+ const [key, value] = member.split("=") as [string, string];
48
+
49
+ expect(W3C_KEY_PATTERN.test(key)).toBe(true);
50
+ expect(W3C_VALUE_PATTERN.test(value)).toBe(true);
51
+ expect(W3C_VALUE_FORBIDDEN.test(value)).toBe(false);
52
+ expect(member.length).toBeLessThanOrEqual(512);
53
+ }
54
+ });
55
+ });
56
+
57
+ describe("parseSessionTraceState", () => {
58
+ it("round-trips what the recorder writes", () => {
59
+ expect(
60
+ parseSessionTraceState(buildSessionTraceStateMember(SESSION_ID)),
61
+ ).toEqual({
62
+ sessionTraceState: {
63
+ sessionId: SESSION_ID,
64
+ syntheticParentSpanId: null,
65
+ },
66
+ remainingTraceState: "",
67
+ });
68
+
69
+ expect(
70
+ parseSessionTraceState(
71
+ buildSessionTraceStateMember(SESSION_ID, PARENT_ID),
72
+ ),
73
+ ).toEqual({
74
+ sessionTraceState: {
75
+ sessionId: SESSION_ID,
76
+ syntheticParentSpanId: PARENT_ID,
77
+ },
78
+ remainingTraceState: "",
79
+ });
80
+ });
81
+
82
+ it("finds the member among other vendors' members and strips only ours", () => {
83
+ const parsed: ParsedSessionTraceState = parseSessionTraceState(
84
+ `dd=s:1;o:rum, oneuptime=sid:${SESSION_ID} ,congo=t61rcWkgMzE,tenant@vendor=x`,
85
+ );
86
+
87
+ expect(parsed.sessionTraceState?.sessionId).toBe(SESSION_ID);
88
+ expect(parsed.remainingTraceState).toBe(
89
+ "dd=s:1;o:rum,congo=t61rcWkgMzE,tenant@vendor=x",
90
+ );
91
+ });
92
+
93
+ it("tolerates optional whitespace and tabs around members and fields", () => {
94
+ const parsed: ParsedSessionTraceState = parseSessionTraceState(
95
+ `\toneuptime = sid:${SESSION_ID} ; p:${PARENT_ID}\t`,
96
+ );
97
+
98
+ expect(parsed.sessionTraceState).toEqual({
99
+ sessionId: SESSION_ID,
100
+ syntheticParentSpanId: PARENT_ID,
101
+ });
102
+ });
103
+
104
+ it("reads fields in any order and ignores unknown ones", () => {
105
+ expect(
106
+ parseSessionTraceState(
107
+ `oneuptime=v:2;p:${PARENT_ID};x:y;sid:${SESSION_ID}`,
108
+ ).sessionTraceState,
109
+ ).toEqual({
110
+ sessionId: SESSION_ID,
111
+ syntheticParentSpanId: PARENT_ID,
112
+ });
113
+ });
114
+
115
+ it("lowercases hex ids", () => {
116
+ expect(
117
+ parseSessionTraceState(
118
+ `oneuptime=sid:${SESSION_ID.toUpperCase()};p:${PARENT_ID.toUpperCase()}`,
119
+ ).sessionTraceState,
120
+ ).toEqual({
121
+ sessionId: SESSION_ID,
122
+ syntheticParentSpanId: PARENT_ID,
123
+ });
124
+ });
125
+
126
+ it("takes the first oneuptime member and strips every one", () => {
127
+ const other: string = "ffffffffffffffffffffffffffffffff";
128
+ const parsed: ParsedSessionTraceState = parseSessionTraceState(
129
+ `oneuptime=sid:${SESSION_ID},a=b,oneuptime=sid:${other}`,
130
+ );
131
+
132
+ expect(parsed.sessionTraceState?.sessionId).toBe(SESSION_ID);
133
+ expect(parsed.remainingTraceState).toBe("a=b");
134
+ });
135
+
136
+ it("strips a malformed member of ours but yields no session", () => {
137
+ for (const member of [
138
+ "oneuptime=sid:not-hex",
139
+ "oneuptime=sid:0123",
140
+ `oneuptime=sid:${SESSION_ID}00`,
141
+ "oneuptime=sid:00000000000000000000000000000000",
142
+ "oneuptime=p:fedcba9876543210",
143
+ "oneuptime=",
144
+ "oneuptime",
145
+ `oneuptime=${SESSION_ID}`,
146
+ ]) {
147
+ const parsed: ParsedSessionTraceState = parseSessionTraceState(
148
+ `${member},keep=me`,
149
+ );
150
+
151
+ expect(parsed.sessionTraceState).toBeNull();
152
+ expect(parsed.remainingTraceState).toBe("keep=me");
153
+ }
154
+ });
155
+
156
+ it("keeps the session when only the parent id is malformed", () => {
157
+ for (const parent of ["xyz", "0000000000000000", "fedcba98765432", ""]) {
158
+ expect(
159
+ parseSessionTraceState(`oneuptime=sid:${SESSION_ID};p:${parent}`)
160
+ .sessionTraceState,
161
+ ).toEqual({ sessionId: SESSION_ID, syntheticParentSpanId: null });
162
+ }
163
+ });
164
+
165
+ it("does not match keys that only contain the word", () => {
166
+ const value: string = `acme@oneuptime=sid:${SESSION_ID},oneuptimex=sid:${SESSION_ID}`;
167
+ const parsed: ParsedSessionTraceState = parseSessionTraceState(value);
168
+
169
+ expect(parsed.sessionTraceState).toBeNull();
170
+ expect(parsed.remainingTraceState).toBe(value);
171
+ });
172
+
173
+ it("returns the value untouched when there is no member of ours", () => {
174
+ expect(parseSessionTraceState("rojo=00f067aa0ba902b7")).toEqual({
175
+ sessionTraceState: null,
176
+ remainingTraceState: "rojo=00f067aa0ba902b7",
177
+ });
178
+ expect(parseSessionTraceState("")).toEqual({
179
+ sessionTraceState: null,
180
+ remainingTraceState: "",
181
+ });
182
+ });
183
+
184
+ it("never throws on non-string input", () => {
185
+ for (const value of [
186
+ undefined,
187
+ null,
188
+ 42,
189
+ true,
190
+ {},
191
+ [`oneuptime=sid:${SESSION_ID}`],
192
+ ]) {
193
+ expect(parseSessionTraceState(value)).toEqual({
194
+ sessionTraceState: null,
195
+ remainingTraceState: "",
196
+ });
197
+ }
198
+ });
199
+
200
+ it("reads no session out of oversized or over-long lists but still strips ours", () => {
201
+ const others: string = "a=b,".repeat(3000);
202
+ const huge: string = `oneuptime=sid:${SESSION_ID};p:${PARENT_ID},${others}`;
203
+ const parsedHuge: ParsedSessionTraceState = parseSessionTraceState(huge);
204
+
205
+ expect(parsedHuge.sessionTraceState).toBeNull();
206
+ expect(parsedHuge.remainingTraceState).toBe("a=b,".repeat(2999) + "a=b");
207
+ expect(parsedHuge.remainingTraceState).not.toContain(SESSION_ID);
208
+
209
+ const many: string = `${"k=v,".repeat(80)}oneuptime=sid:${SESSION_ID}`;
210
+ const parsedMany: ParsedSessionTraceState = parseSessionTraceState(many);
211
+
212
+ expect(parsedMany.sessionTraceState).toBeNull();
213
+ expect(parsedMany.remainingTraceState).toBe("k=v,".repeat(79) + "k=v");
214
+ });
215
+
216
+ it("leaves an oversized list with no member of ours untouched", () => {
217
+ const huge: string = "a=b,".repeat(3000);
218
+ expect(parseSessionTraceState(huge)).toEqual({
219
+ sessionTraceState: null,
220
+ remainingTraceState: huge,
221
+ });
222
+ });
223
+
224
+ it("accepts a long but legitimate list that ends with our member", () => {
225
+ const others: Array<string> = [];
226
+
227
+ for (let i: number = 0; i < 20; i++) {
228
+ others.push(`vendor${i}=${"x".repeat(30)}`);
229
+ }
230
+
231
+ const value: string = `${others.join(",")},oneuptime=sid:${SESSION_ID}`;
232
+ expect(value.length).toBeGreaterThan(600);
233
+
234
+ const parsed: ParsedSessionTraceState = parseSessionTraceState(value);
235
+ expect(parsed.sessionTraceState?.sessionId).toBe(SESSION_ID);
236
+ expect(parsed.remainingTraceState).toBe(others.join(","));
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,38 @@
1
+ /*
2
+ * Shared between the dashboard and the server for linking alerts to incidents
3
+ * (the IncidentAlert model).
4
+ */
5
+
6
+ /*
7
+ * Declaring an incident from alerts sends the alert ids along with the
8
+ * incident as `miscDataProps[INCIDENT_ALERT_IDS_TO_LINK_KEY]` (an array of id
9
+ * strings). IncidentService validates them before the incident is created and
10
+ * links them once it exists, so API clients get the same one-request flow as
11
+ * the dashboard.
12
+ */
13
+ export const INCIDENT_ALERT_IDS_TO_LINK_KEY: string = "alertIdsToLink";
14
+
15
+ /*
16
+ * The create-incident page reads the alerts to declare an incident from out of
17
+ * this query string parameter, as a comma separated list of alert ids.
18
+ */
19
+ export const INCIDENT_CREATE_ALERT_IDS_QUERY_PARAM: string = "alertIds";
20
+
21
+ /*
22
+ * The most alerts one action can link to an incident: declaring an incident
23
+ * from alerts, or linking a selection of alerts from the alerts table. Every
24
+ * link writes a feed entry on the incident and the alert, and the incident's
25
+ * entry is posted to its Slack / Microsoft Teams channels, so an unbounded
26
+ * "select all" would flood both. It also keeps the alert ids within what a URL
27
+ * can carry.
28
+ */
29
+ export const MAX_ALERTS_PER_INCIDENT_LINK_ACTION: number = 50;
30
+
31
+ /*
32
+ * What linking a pair that is already linked answers with - whether the
33
+ * duplicate is caught by the model's unique-together check or, when two
34
+ * requests race past it, by the unique index itself. The dashboard counts a
35
+ * link that fails with this message as done.
36
+ */
37
+ export const INCIDENT_ALERT_ALREADY_LINKED_MESSAGE: string =
38
+ "This alert is already linked to this incident.";
@@ -1851,6 +1851,12 @@ enum Permission {
1851
1851
  EditIncidentEpisodeMember = "EditIncidentEpisodeMember",
1852
1852
  ReadIncidentEpisodeMember = "ReadIncidentEpisodeMember",
1853
1853
 
1854
+ // Incident Alert (alerts linked to an incident) Permissions
1855
+ CreateIncidentAlert = "CreateIncidentAlert",
1856
+ DeleteIncidentAlert = "DeleteIncidentAlert",
1857
+ EditIncidentAlert = "EditIncidentAlert",
1858
+ ReadIncidentAlert = "ReadIncidentAlert",
1859
+
1854
1860
  // Incident Episode State Timeline Permissions
1855
1861
  CreateIncidentEpisodeStateTimeline = "CreateIncidentEpisodeStateTimeline",
1856
1862
  DeleteIncidentEpisodeStateTimeline = "DeleteIncidentEpisodeStateTimeline",
@@ -15015,6 +15021,48 @@ export class PermissionHelper {
15015
15021
  group: PermissionGroup.Incident,
15016
15022
  },
15017
15023
 
15024
+ // Incident Alert (alerts linked to an incident) Permissions
15025
+ {
15026
+ permission: Permission.CreateIncidentAlert,
15027
+ title: "Create Incident Alert",
15028
+ description:
15029
+ "This permission can link alerts to incidents in this project.",
15030
+ isAssignableToTenant: true,
15031
+ isAccessControlPermission: false,
15032
+ isRolePermission: false,
15033
+ group: PermissionGroup.Incident,
15034
+ },
15035
+ {
15036
+ permission: Permission.DeleteIncidentAlert,
15037
+ title: "Delete Incident Alert",
15038
+ description:
15039
+ "This permission can unlink alerts from incidents in this project.",
15040
+ isAssignableToTenant: true,
15041
+ isAccessControlPermission: false,
15042
+ isRolePermission: false,
15043
+ group: PermissionGroup.Incident,
15044
+ },
15045
+ {
15046
+ permission: Permission.EditIncidentAlert,
15047
+ title: "Edit Incident Alert",
15048
+ description:
15049
+ "This permission can edit the links between alerts and incidents in this project.",
15050
+ isAssignableToTenant: true,
15051
+ isAccessControlPermission: false,
15052
+ isRolePermission: false,
15053
+ group: PermissionGroup.Incident,
15054
+ },
15055
+ {
15056
+ permission: Permission.ReadIncidentAlert,
15057
+ title: "Read Incident Alert",
15058
+ description:
15059
+ "This permission can read which alerts are linked to which incidents in this project.",
15060
+ isAssignableToTenant: true,
15061
+ isAccessControlPermission: false,
15062
+ isRolePermission: false,
15063
+ group: PermissionGroup.Incident,
15064
+ },
15065
+
15018
15066
  // Incident Episode State Timeline Permissions
15019
15067
  {
15020
15068
  permission: Permission.CreateIncidentEpisodeStateTimeline,
@@ -851,19 +851,38 @@ export interface SessionReplayConfigResponse {
851
851
  */
852
852
 
853
853
  /*
854
- * Origins the recorder may inject a W3C traceparent header into, so a
855
- * recording links to the backend trace of the request that failed —
856
- * with NO OTel browser instrumentation on the customer's page.
854
+ * CROSS-origin APIs the recorder may inject a W3C traceparent header
855
+ * into, so a recording links to the backend trace of the request that
856
+ * failed — with NO OTel browser instrumentation on the customer's page.
857
+ * Listed origins get traceparent only, never the session id. The page's
858
+ * own origin needs no entry: see sameOriginTracePropagation.
857
859
  *
858
- * Empty means never inject, and that default is the safety mechanism:
859
- * adding a request header turns a simple cross-origin request into a
860
- * preflighted one, and an API that does not allow traceparent in
860
+ * Empty means no cross-origin injection, and that default is the safety
861
+ * mechanism: adding a request header turns a simple cross-origin request
862
+ * into a preflighted one, and an API that does not allow traceparent in
861
863
  * Access-Control-Allow-Headers would start failing because a RUM
862
864
  * script was installed. Each listed origin is an explicit statement
863
865
  * that its API accepts the header.
864
866
  */
865
867
  tracePropagationOrigins?: Array<string>;
866
868
 
869
+ /*
870
+ * RumApplication.sessionReplaySameOriginTracePropagation. When true, the
871
+ * recorder adds a traceparent (unless the request or an inner tracing
872
+ * instrumentation already sets one) and a tracestate member carrying the
873
+ * replay session id (`oneuptime=sid:<id>`, see Utils/Rum/SessionTraceState)
874
+ * to fetch/XHR requests to the page's OWN origin, and only while the
875
+ * session is uploading with consent. Span ingest reads the member back,
876
+ * so backend spans link to the recording with no customer code.
877
+ *
878
+ * A same-origin request is never preflighted, so this needs no origin
879
+ * list and defaults on; the switch turns it off per application without
880
+ * a customer redeploy. The server always sends an explicit boolean (false
881
+ * on every disabled response) and the recorder reads anything but `true`
882
+ * as off, so an older server or a cached stub never turns it on.
883
+ */
884
+ sameOriginTracePropagation?: boolean;
885
+
867
886
  /*
868
887
  * Performance capture budgets, milliseconds; 0 disables that trigger.
869
888
  * Exceeding a budget fires the Performance capture trigger, so slow
@@ -1,6 +1,12 @@
1
1
  export interface WebVitalDefinition {
2
2
  key: string;
3
3
  label: string;
4
+ /*
5
+ * What the vital measures, in words a non-specialist can act on. The
6
+ * Good / Poor limits are not repeated here - describeWebVitalThresholds
7
+ * derives them from `thresholds`, so the two cannot disagree.
8
+ */
9
+ description: string;
4
10
  unit: "ms" | "score";
5
11
  thresholds: { warn: number; danger: number };
6
12
  names: Array<string>;
@@ -11,6 +17,8 @@ export const WebVitalDefinitions: Array<WebVitalDefinition> = [
11
17
  {
12
18
  key: "lcp",
13
19
  label: "Largest Contentful Paint",
20
+ description:
21
+ "How long until the biggest image or block of text on screen has rendered - roughly when the page looks loaded to the visitor.",
14
22
  unit: "ms",
15
23
  thresholds: { warn: 2500, danger: 4000 },
16
24
  names: [
@@ -23,6 +31,8 @@ export const WebVitalDefinitions: Array<WebVitalDefinition> = [
23
31
  {
24
32
  key: "inp",
25
33
  label: "Interaction to Next Paint",
34
+ description:
35
+ "How quickly the page reacts to clicks, taps and key presses: the delay before the screen visibly updates, for close to the slowest interaction on the page.",
26
36
  unit: "ms",
27
37
  thresholds: { warn: 200, danger: 500 },
28
38
  names: [
@@ -35,6 +45,8 @@ export const WebVitalDefinitions: Array<WebVitalDefinition> = [
35
45
  {
36
46
  key: "cls",
37
47
  label: "Cumulative Layout Shift",
48
+ description:
49
+ "How much the page jumps around while it loads - content moving under the visitor's cursor or finger. A score, not a time: lower is better and 0 means nothing moved.",
38
50
  unit: "score",
39
51
  thresholds: { warn: 0.1, danger: 0.25 },
40
52
  names: [
@@ -47,6 +59,8 @@ export const WebVitalDefinitions: Array<WebVitalDefinition> = [
47
59
  {
48
60
  key: "fcp",
49
61
  label: "First Contentful Paint",
62
+ description:
63
+ "How long until the first text or image appears, so the visitor can see the page has started loading.",
50
64
  unit: "ms",
51
65
  thresholds: { warn: 1800, danger: 3000 },
52
66
  names: [
@@ -59,6 +73,8 @@ export const WebVitalDefinitions: Array<WebVitalDefinition> = [
59
73
  {
60
74
  key: "ttfb",
61
75
  label: "Time to First Byte",
76
+ description:
77
+ "How long the browser waited for the first byte of the page from your server, including redirects, looking up the address and connecting.",
62
78
  unit: "ms",
63
79
  thresholds: { warn: 800, danger: 1800 },
64
80
  names: [
@@ -69,3 +85,39 @@ export const WebVitalDefinitions: Array<WebVitalDefinition> = [
69
85
  ],
70
86
  },
71
87
  ];
88
+
89
+ // A threshold the way a person would say it: "2.5 s", "800 ms", "0.1".
90
+ export function formatWebVitalThreshold(
91
+ value: number,
92
+ unit: WebVitalDefinition["unit"],
93
+ ): string {
94
+ if (unit === "score") {
95
+ return String(value);
96
+ }
97
+
98
+ if (value < 1000) {
99
+ return `${value} ms`;
100
+ }
101
+
102
+ return `${Number((value / 1000).toFixed(2))} s`;
103
+ }
104
+
105
+ /*
106
+ * The Good / Poor limits in words, matching how the overview rates a value:
107
+ * below `warn` is Good, at or above `danger` is Poor, and anything between
108
+ * needs work.
109
+ */
110
+ export function describeWebVitalThresholds(
111
+ definition: Pick<WebVitalDefinition, "unit" | "thresholds">,
112
+ ): string {
113
+ const warn: string = formatWebVitalThreshold(
114
+ definition.thresholds.warn,
115
+ definition.unit,
116
+ );
117
+ const danger: string = formatWebVitalThreshold(
118
+ definition.thresholds.danger,
119
+ definition.unit,
120
+ );
121
+
122
+ return `Good below ${warn}; poor at ${danger} or more.`;
123
+ }
@@ -1,6 +1,7 @@
1
1
  import Link from "../Link/Link";
2
2
  import { DetailSideLink } from "./Field";
3
3
  import useTranslateValue from "../../Utils/Translation";
4
+ import InfoTooltip from "../Tooltip/InfoTooltip";
4
5
  import React, { FunctionComponent, ReactElement } from "react";
5
6
 
6
7
  export enum Size {
@@ -16,6 +17,8 @@ export interface ComponentProps {
16
17
  sideLink?: DetailSideLink | undefined;
17
18
  size?: Size | undefined;
18
19
  isCardStyle?: boolean | undefined;
20
+ // What the field means, shown in an (i) tooltip beside the title.
21
+ tooltip?: string | undefined;
19
22
  }
20
23
 
21
24
  const FieldLabelElement: FunctionComponent<ComponentProps> = (
@@ -40,6 +43,11 @@ const FieldLabelElement: FunctionComponent<ComponentProps> = (
40
43
  >
41
44
  <span className={`${props.alignClassName} flex items-center gap-1.5`}>
42
45
  {translatedTitle}
46
+ <InfoTooltip
47
+ label={translatedTitle}
48
+ text={props.tooltip}
49
+ className="relative z-10 normal-case tracking-normal"
50
+ />
43
51
  <span className="w-1 h-1 rounded-full bg-gray-300"></span>
44
52
  </span>
45
53
  {props.sideLink && translatedSideLinkText && props.sideLink?.url && (
@@ -20,6 +20,8 @@ export interface ComponentProps {
20
20
  isSelected?: boolean | undefined;
21
21
  // Announced in place of the title when the card is clickable.
22
22
  ariaLabel?: string | undefined;
23
+ // What the value means, shown in an (i) tooltip beside the title.
24
+ tooltip?: string | undefined;
23
25
  }
24
26
 
25
27
  const InfoCard: FunctionComponent<ComponentProps> = (
@@ -37,6 +39,50 @@ const InfoCard: FunctionComponent<ComponentProps> = (
37
39
  ? "border-indigo-500 ring-1 ring-indigo-500"
38
40
  : "border-gray-200";
39
41
 
42
+ /*
43
+ * A clickable card with a tooltip cannot stay a role="button" div: the (i)
44
+ * is a button of its own, and a button's children are presentational, so
45
+ * screen readers would never reach it. The card becomes a plain container
46
+ * with a real button laid over it, and the (i) sits above that button
47
+ * (FieldLabel lifts it with relative z-10). Cards without a tooltip keep
48
+ * the markup below, unchanged.
49
+ */
50
+ if (isClickable && props.tooltip?.trim()) {
51
+ return (
52
+ <div
53
+ className={`relative rounded-xl bg-white border ${borderClassName} shadow-sm hover:shadow-md transition-shadow duration-200 p-5 ${props.className || ""}`}
54
+ >
55
+ <button
56
+ type="button"
57
+ onClick={props.onClick}
58
+ aria-pressed={
59
+ props.isSelected !== undefined
60
+ ? Boolean(props.isSelected)
61
+ : undefined
62
+ }
63
+ /*
64
+ * Named like the old role="button" card, which read its title and
65
+ * value - unless the value is an element, which has no text to
66
+ * borrow here.
67
+ */
68
+ aria-label={
69
+ props.ariaLabel ||
70
+ (typeof props.value === "string"
71
+ ? `${props.title} ${props.value}`
72
+ : props.title)
73
+ }
74
+ className="absolute inset-0 rounded-xl cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
75
+ />
76
+ <div className="mb-2">
77
+ <FieldLabelElement title={props.title} tooltip={props.tooltip} />
78
+ </div>
79
+ <div className={props.textClassName || "text-gray-900"}>
80
+ {props.value}
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+
40
86
  return (
41
87
  <div
42
88
  onClick={props.onClick}
@@ -75,7 +121,7 @@ const InfoCard: FunctionComponent<ComponentProps> = (
75
121
  className={`rounded-xl bg-white border ${borderClassName} shadow-sm hover:shadow-md transition-shadow duration-200 p-5 ${isClickable ? "cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2" : ""} ${props.className || ""}`}
76
122
  >
77
123
  <div className="mb-2">
78
- <FieldLabelElement title={props.title} />
124
+ <FieldLabelElement title={props.title} tooltip={props.tooltip} />
79
125
  </div>
80
126
  <div className={props.textClassName || "text-gray-900"}>
81
127
  {props.value}