Skip to content
Source
Sign inSign up
Open History to see the last commit

DiagnosticsSettings.tsx

1,321 lines48.4 KB

1import { ProcessSignalActions } from "./ProcessSignalActions";
2import { resolveUsageAccess } from "@t3tools/client-runtime/state/usage-access";
3import { environmentSession } from "../../state/session";
4import { AuthOrchestrationOperateScope } from "@t3tools/contracts";
5import { readEnvironmentScope, useEnvironmentScope } from "../../state/session";
6import { AuthEnvironmentMaintainScope } from "@t3tools/contracts";
7import { RefreshIcon } from "~/components/ui/refresh-icon";
8import {
9 AlertTriangleIcon,
10 CopyIcon,
11 FolderOpenIcon,
12 InfoIcon,
13 ChevronDownIcon,
14 ChevronRightIcon,
15} from "lucide-react";
16import {
17 isAtomCommandInterrupted,
18 squashAtomCommandFailure,
19} from "@t3tools/client-runtime/state/runtime";
20import { useRef, useState, type ReactNode } from "react";
21import type {
22 ServerProcessDiagnosticsEntry,
23 ServerProcessResourceHistorySummary,
24 ServerProcessSignal,
25} from "@t3tools/contracts";
26import * as DateTime from "effect/DateTime";
27import * as Option from "effect/Option";
28​
29import { formatBytes } from "../../lib/formatBytes";
30import { cn } from "../../lib/utils";
31import { ensureLocalApi } from "../../localApi";
32import { useOpenInPreferredEditor } from "../../editorPreferences";
33import { useRelativeTimeState } from "../../hooks/useRelativeTimeState";
34import { useEnvironmentQuery } from "../../state/query";
35import { serverEnvironment } from "../../state/server";
36import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
37import { Button } from "../ui/button";
38import { ScrollArea } from "../ui/scroll-area";
39import { Toggle, ToggleGroup } from "../ui/toggle-group";
40import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
41import { toastManager } from "../ui/toast";
42import { formatDiagnosticsDuration } from "./DiagnosticsSettings.logic";
43import { ExpandableText } from "./ExpandableText";
44import { ResourceTelemetryDiagnostics } from "./ResourceTelemetryDiagnostics";
45import { formatCpuTime } from "./ResourceTelemetryDiagnostics.logic";
46import { SettingsPageContainer, SettingsSection } from "./settingsLayout";
47import { useAtomCommand } from "../../state/use-atom-command";
48import { useSettingsScope } from "./SettingsScopeContext";
49import { TelemetryExportSettings } from "./TelemetryExportSettings";
50​
51const NUMBER_FORMAT = new Intl.NumberFormat();
52​
53function formatCount(value: number): string {
54 return NUMBER_FORMAT.format(value);
56​
57function RelativeTimeNoWrap(props: { value: DateTime.Utc }) {
58 const relative = useRelativeTimeState(DateTime.formatIso(props.value));
59 if (relative.status !== "relative") return null;
60 return relative.suffix ? `${relative.value}\u00a0${relative.suffix}` : relative.value;
62​
63function shortenTraceId(traceId: string): string {
64 if (traceId.length <= 32) return traceId;
65 return `${traceId.slice(0, 18)}...${traceId.slice(-10)}`;
67​
68function isStaleProcessSignalMessage(message: string | undefined): boolean {
69 return message?.includes("not a live descendant") ?? false;
71​
72function StatBlock({
73 label,
74 value,
75 tooltip,
76 tone = "default",
77}: {
78 label: string;
79 value: string;
80 tooltip?: ReactNode;
81 tone?: "default" | "warning" | "danger";
82}) {
83 return (
84 <div className="min-w-0 border-border/60 px-4 py-3 sm:px-5">
85 <div className="flex min-w-0 items-center gap-1.5 text-2xs font-medium uppercase tracking-widest text-muted-foreground/70">
86 <span className="min-w-0 truncate">{label}</span>
87 {tooltip ? (
88 <Tooltip>
89 <TooltipTrigger
90 render={
91 <button
92 type="button"
93 className="cursor-pointer inline-flex size-3.5 shrink-0 items-center justify-center rounded-sm text-muted-foreground/60 hover:text-foreground"
94 aria-label={`${label} details`}
95 >
96 <InfoIcon className="size-3" />
97 </button>
98 }
99 />
100 <TooltipPopup side="top">{tooltip}</TooltipPopup>
101 </Tooltip>
102 ) : null}
103 </div>
104 <div
105 className={cn(
106 "mt-1 truncate font-mono text-lg font-semibold tabular-nums text-foreground",
107 tone === "warning" && "text-warning-foreground",
108 tone === "danger" && "text-destructive",
109 )}
110 >
111 {value}
112 </div>
113 </div>
114 );
116​
117function StatsGrid({ children }: { children: ReactNode }) {
118 return (
119 <div className="relative grid grid-cols-2 sm:grid-cols-4">
120 <span
121 className="pointer-events-none absolute inset-y-0 left-1/2 w-px bg-border/60"
122 aria-hidden
123 />
124 <span
125 className="pointer-events-none absolute inset-x-0 top-1/2 h-px bg-border/60 sm:hidden"
126 aria-hidden
127 />
128 <span
129 className="pointer-events-none absolute inset-y-0 left-1/4 hidden w-px bg-border/60 sm:block"
130 aria-hidden
131 />
132 <span
133 className="pointer-events-none absolute inset-y-0 left-3/4 hidden w-px bg-border/60 sm:block"
134 aria-hidden
135 />
136 {children}
137 </div>
138 );
140​
141function EmptyRows({ label }: { label: string }) {
142 return <div className="px-4 py-4 text-xs text-muted-foreground sm:px-5">{label}</div>;
144​
145function DiagnosticsTable({
146 headers,
147 children,
148 minTableWidth = "min-w-[640px]",
149 columnWidths,
150}: {
151 headers: ReadonlyArray<string>;
152 children: ReactNode;
153 minTableWidth?: string;
154 columnWidths?: ReadonlyArray<string>;
155}) {
156 return (
157 <ScrollArea
158 radius="none"
159 chainVerticalScroll
160 scrollFade
161 hideScrollbars
162 className="w-full max-w-full"
163 >
164 <table
165 className={cn("w-full text-left text-xs", minTableWidth, columnWidths && "table-fixed")}
166 >
167 {columnWidths ? (
168 <colgroup>
169 {headers.map((header, index) => (
170 <col key={header} className={columnWidths[index]} />
171 ))}
172 </colgroup>
173 ) : null}
174 <thead className="border-b border-border/60 text-2xs uppercase tracking-widest text-muted-foreground/70">
175 <tr>
176 {headers.map((header, index) => (
177 <th
178 key={header}
179 className={cn(
180 "whitespace-nowrap px-4 py-2.5 font-semibold first:sm:pl-5 last:sm:pr-5",
181 !columnWidths && index === headers.length - 1 && "w-px",
182 )}
183 >
184 {header.replaceAll(" ", "\u00a0")}
185 </th>
186 ))}
187 </tr>
188 </thead>
189 <tbody className="divide-y divide-border/60">{children}</tbody>
190 </table>
191 </ScrollArea>
192 );
194​
195function TraceIdCell({ traceId }: { traceId: string }) {
196 const { copyToClipboard, isCopied: copied } = useCopyToClipboard({
197 target: "trace ID",
198 timeout: 1_200,
199 });
200​
201 return (
202 <div className="flex w-full min-w-0 max-w-full items-center gap-2">
203 <Tooltip>
204 <TooltipTrigger
205 render={
206 <span className="min-w-0 flex-1 truncate font-mono text-2xs">
207 {shortenTraceId(traceId)}
208 </span>
209 }
210 />
211 <TooltipPopup side="top" variant="code">
212 {traceId}
213 </TooltipPopup>
214 </Tooltip>
215 <Tooltip>
216 <TooltipTrigger
217 render={
218 <Button
219 size="icon-micro"
220 variant="ghost-muted"
221 aria-label={copied ? "Copied trace ID" : "Copy trace ID"}
222 onClick={() => copyToClipboard(traceId)}
223 >
224 <CopyIcon className="size-3" />
225 </Button>
226 }
227 />
228 <TooltipPopup side="top">{copied ? "Copied" : "Copy full trace ID"}</TooltipPopup>
229 </Tooltip>
230 </div>
231 );
233​
234function formatProcessName(command: string): string {
235 const firstToken = command.trim().split(/\s+/)[0];
236 if (!firstToken) return command;
237 const normalized = firstToken.replace(/^['"]|['"]$/g, "");
238 const segments = normalized.split(/[\\/]/).filter(Boolean);
239 return segments.at(-1) ?? normalized;
241​
242function formatProcessType(process: ServerProcessDiagnosticsEntry): string {
243 if (process.depth > 0) return "Subprocess";
244 if (/\b(codex|claude|opencode|cursor)\b/i.test(process.command)) return "Agent";
245 return "Process";
247​
248function ProcessNameCell({
249 process,
250 isExpanded,
251 onToggle,
252}: {
253 process: ServerProcessDiagnosticsEntry;
254 isExpanded: boolean;
255 onToggle: (pid: number) => void;
256}) {
257 const name = formatProcessName(process.command);
258 const hasChildren = process.childPids.length > 0;
259​
260 return (
261 <div
262 className="grid min-w-0 grid-cols-[1.25rem_0.375rem_minmax(0,1fr)] items-center gap-2"
263 style={{ paddingLeft: `${Math.min(process.depth, 6) * 10}px` }}
264 >
265 {hasChildren ? (
266 <Button
267 size="icon-micro"
268 variant="ghost-muted"
269 aria-label={isExpanded ? `Collapse ${name}` : `Expand ${name}`}
270 onClick={() => onToggle(process.pid)}
271 >
272 {isExpanded ? (
273 <ChevronDownIcon className="size-3.5" />
274 ) : (
275 <ChevronRightIcon className="size-3.5" />
276 )}
277 </Button>
278 ) : (
279 <span className="size-5 shrink-0" aria-hidden="true" />
280 )}
281 <span className="size-1.5 shrink-0 rounded-full bg-success/80" />
282 <Tooltip>
283 <TooltipTrigger
284 render={<span className="min-w-0 truncate font-medium text-foreground">{name}</span>}
285 />
286 <TooltipPopup side="top" variant="code">
287 {process.command}
288 </TooltipPopup>
289 </Tooltip>
290 </div>
291 );
293​
294function visibleProcessTree(
295 processes: ReadonlyArray<ServerProcessDiagnosticsEntry>,
296 collapsedPids: ReadonlySet<number>,
297) {
298 const visible: ServerProcessDiagnosticsEntry[] = [];
299 let hiddenChildDepth: number | null = null;
300​
301 for (const process of processes) {
302 if (hiddenChildDepth !== null) {
303 if (process.depth > hiddenChildDepth) continue;
304 hiddenChildDepth = null;
305 }
306​
307 visible.push(process);
308 if (collapsedPids.has(process.pid)) {
309 hiddenChildDepth = process.depth;
310 }
311 }
312​
313 return visible;
315​
316function ProcessDiagnosticsTable({
317 processes,
318 canMaintainEnvironment,
319 signalingPid,
320 onSignal,
321 emptyLabel,
322}: {
323 processes: ReadonlyArray<ServerProcessDiagnosticsEntry>;
324 canMaintainEnvironment: boolean;
325 signalingPid: number | null;
326 onSignal: (pid: number, signal: ServerProcessSignal) => void;
327 emptyLabel?: string;
328}) {
329 const [collapsedPids, setCollapsedPids] = useState<ReadonlySet<number>>(() => new Set());
330 const visibleProcesses = visibleProcessTree(processes, collapsedPids);
331​
332 const toggleProcess = (pid: number) => {
333 setCollapsedPids((previous) => {
334 const next = new Set(previous);
335 if (next.has(pid)) {
336 next.delete(pid);
337 } else {
338 next.add(pid);
339 }
340 return next;
341 });
342 };
343​
344 return (
345 <div className="border-t border-border/60">
346 <ScrollArea
347 radius="none"
348 chainVerticalScroll
349 scrollFade
350 hideScrollbars
351 className="max-h-[min(64vh,44rem)] w-full max-w-full"
352 >
353 <table className="w-full min-w-[1040px] table-fixed text-left text-xs">
354 <colgroup>
355 <col className="w-[24%]" />
356 <col className="w-[8%]" />
357 <col className="w-[10%]" />
358 <col className="w-[33%]" />
359 <col className="w-[8%]" />
360 <col className="w-[11%]" />
361 <col className="w-[6%]" />
362 </colgroup>
363 <thead className="sticky top-0 z-10 border-b border-border/60 bg-card text-2xs uppercase tracking-widest text-muted-foreground/70">
364 <tr>
365 <th className="px-4 py-2 font-semibold sm:pl-5">Name</th>
366 <th className="px-3 py-2 text-right font-semibold">CPU</th>
367 <th className="px-3 py-2 text-right font-semibold">Memory</th>
368 <th className="px-3 py-2 font-semibold">Command</th>
369 <th className="px-3 py-2 text-right font-semibold">PID</th>
370 <th className="px-3 py-2 font-semibold">Type</th>
371 <th className="p-2 text-right font-semibold sm:pr-4">Kill</th>
372 </tr>
373 </thead>
374 <tbody className="divide-y divide-border/50">
375 {visibleProcesses.length === 0 ? (
376 <tr>
377 <td colSpan={7} className="px-4 py-4 text-xs text-muted-foreground sm:px-5">
378 {emptyLabel ?? "No live descendant processes found."}
379 </td>
380 </tr>
381 ) : null}
382 {visibleProcesses.map((process) => (
383 <tr key={process.pid} className="hover:bg-muted/20">
384 <td className="px-4 py-2 align-middle sm:pl-5">
385 <ProcessNameCell
386 process={process}
387 isExpanded={!collapsedPids.has(process.pid)}
388 onToggle={toggleProcess}
389 />
390 </td>
391 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
392 {process.cpuPercent.toFixed(1)}%
393 </td>
394 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
395 {formatBytes(process.rssBytes)}
396 </td>
397 <td className="px-3 py-2 align-middle text-muted-foreground">
398 <Tooltip>
399 <TooltipTrigger
400 render={<span className="block truncate">{process.command}</span>}
401 />
402 <TooltipPopup side="top" variant="code">
403 {process.command}
404 </TooltipPopup>
405 </Tooltip>
406 </td>
407 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums text-muted-foreground">
408 {process.pid}
409 </td>
410 <td className="truncate px-3 py-2 align-middle text-muted-foreground">
411 {formatProcessType(process)}
412 </td>
413 <td className="p-2 align-middle sm:pr-4">
414 <ProcessSignalActions
415 disabled={!canMaintainEnvironment || signalingPid === process.pid}
416 onSignal={(signal) => onSignal(process.pid, signal)}
417 />
418 </td>
419 </tr>
420 ))}
421 </tbody>
422 </table>
423 </ScrollArea>
424 </div>
425 );
427​
428const RESOURCE_HISTORY_WINDOWS = [
429 { label: "5m", windowMs: 5 * 60_000, bucketMs: 30_000 },
430 { label: "15m", windowMs: 15 * 60_000, bucketMs: 60_000 },
431 { label: "30m", windowMs: 30 * 60_000, bucketMs: 2 * 60_000 },
432 { label: "1h", windowMs: 60 * 60_000, bucketMs: 5 * 60_000 },
433] as const;
434​
435function formatShortProcessName(command: string): string {
436 const name = formatProcessName(command);
437 return name.length > 42 ? `${name.slice(0, 39)}...` : name;
439​
440function ResourceHistoryProcessNameCell({
441 process,
442 visualDepth,
443}: {
444 process: ServerProcessResourceHistorySummary;
445 visualDepth: number;
446}) {
447 const name = formatShortProcessName(process.command);
448​
449 return (
450 <div
451 className="grid min-w-0 grid-cols-[1.25rem_0.375rem_minmax(0,1fr)] items-center gap-2"
452 style={{ paddingLeft: `${Math.min(visualDepth, 6) * 10}px` }}
453 aria-label={`${process.isServerRoot ? "Root" : "Child"} process ${name}`}
454 >
455 <span className="size-5 shrink-0" aria-hidden="true" />
456 <span
457 className={cn(
458 "size-1.5 shrink-0 rounded-full",
459 process.isServerRoot ? "bg-warning/90" : "bg-success/80",
460 )}
461 />
462 <Tooltip>
463 <TooltipTrigger
464 render={<span className="min-w-0 truncate font-medium text-foreground">{name}</span>}
465 />
466 <TooltipPopup side="top" variant="code">
467 {process.command}
468 </TooltipPopup>
469 </Tooltip>
470 </div>
471 );
473​
474function ProcessResourceHistoryChart({
475 buckets,
476}: {
477 buckets: ReadonlyArray<{
478 readonly startedAt: DateTime.Utc;
479 readonly avgCpuPercent: number;
480 readonly maxCpuPercent: number;
481 }>;
482}) {
483 const maxCpuPercent = Math.max(1, ...buckets.map((bucket) => bucket.maxCpuPercent));
484​
485 return (
486 <div className="border-t border-border/60 px-4 py-3 sm:px-5">
487 <div className="flex h-28 items-end gap-1 overflow-hidden rounded-sm bg-muted/10 p-2">
488 {buckets.map((bucket) => {
489 const peakHeight = Math.max(2, (bucket.maxCpuPercent / maxCpuPercent) * 100);
490 const averageHeight = Math.max(2, (bucket.avgCpuPercent / maxCpuPercent) * 100);
491 return (
492 <Tooltip key={DateTime.formatIso(bucket.startedAt)}>
493 <TooltipTrigger
494 render={
495 <div className="flex h-full min-w-1 flex-1 items-end">
496 <div
497 className="relative h-full w-full"
498 aria-label={`Average CPU ${bucket.avgCpuPercent.toFixed(1)}%, peak CPU ${bucket.maxCpuPercent.toFixed(1)}%`}
499 >
500 <div
501 className="absolute inset-x-0 bottom-0 rounded-t-sm bg-foreground/15 transition-colors"
502 style={{ height: `${peakHeight}%` }}
503 />
504 <div
505 className="absolute inset-x-0 bottom-0 rounded-t-sm bg-foreground/60 transition-colors"
506 style={{ height: `${averageHeight}%` }}
507 />
508 </div>
509 </div>
510 }
511 />
512 <TooltipPopup side="top">
513 Avg {bucket.avgCpuPercent.toFixed(1)}%, peak {bucket.maxCpuPercent.toFixed(1)}%
514 </TooltipPopup>
515 </Tooltip>
516 );
517 })}
518 </div>
519 </div>
520 );
522​
523function ResourceHistoryWindowSelector({
524 selectedWindowMs,
525 onSelect,
526}: {
527 selectedWindowMs: number;
528 onSelect: (windowMs: number) => void;
529}) {
530 return (
531 <ToggleGroup
532 aria-label="Process history period"
533 variant="segmented"
534 value={[String(selectedWindowMs)]}
535 onValueChange={(next) => {
536 const selected = RESOURCE_HISTORY_WINDOWS.find(
537 (option) => String(option.windowMs) === next[0],
538 );
539 if (selected) onSelect(selected.windowMs);
540 }}
541 >
542 {RESOURCE_HISTORY_WINDOWS.map((option) => (
543 <Toggle key={option.windowMs} value={String(option.windowMs)}>
544 {option.label}
545 </Toggle>
546 ))}
547 </ToggleGroup>
548 );
550​
551function ProcessResourceHistoryTable({
552 processes,
553 emptyLabel,
554}: {
555 processes: ReadonlyArray<ServerProcessResourceHistorySummary>;
556 emptyLabel: string;
557}) {
558 const shallowestChildDepth = processes.reduce<number | null>((minDepth, process) => {
559 if (process.isServerRoot) return minDepth;
560 return minDepth === null ? process.depth : Math.min(minDepth, process.depth);
561 }, null);
562​
563 return (
564 <div className="border-t border-border/60">
565 <ScrollArea
566 chainVerticalScroll
567 scrollFade
568 hideScrollbars
569 className="max-h-[min(64vh,44rem)] w-full max-w-full"
570 >
571 <table className="w-full min-w-[980px] table-fixed text-left text-xs">
572 <colgroup>
573 <col className="w-[24%]" />
574 <col className="w-[10%]" />
575 <col className="w-[10%]" />
576 <col className="w-[10%]" />
577 <col className="w-[10%]" />
578 <col className="w-[10%]" />
579 <col className="w-[16%]" />
580 <col className="w-[10%]" />
581 </colgroup>
582 <thead className="sticky top-0 z-10 border-b border-border/60 bg-card text-2xs uppercase tracking-widest text-muted-foreground/70">
583 <tr>
584 <th className="px-4 py-2 font-semibold sm:pl-5">Process</th>
585 <th className="px-3 py-2 text-right font-semibold">CPU Time</th>
586 <th className="px-3 py-2 text-right font-semibold">Current</th>
587 <th className="px-3 py-2 text-right font-semibold">Average</th>
588 <th className="px-3 py-2 text-right font-semibold">Peak</th>
589 <th className="px-3 py-2 text-right font-semibold">Max Mem</th>
590 <th className="px-3 py-2 font-semibold">Command</th>
591 <th className="px-3 py-2 text-right font-semibold sm:pr-5">PID</th>
592 </tr>
593 </thead>
594 <tbody className="divide-y divide-border/50">
595 {processes.length === 0 ? (
596 <tr>
597 <td colSpan={8} className="px-4 py-4 text-xs text-muted-foreground sm:px-5">
598 {emptyLabel}
599 </td>
600 </tr>
601 ) : null}
602 {processes.map((process) => (
603 <tr key={process.processKey} className="hover:bg-muted/20">
604 <td className="px-4 py-2 align-middle sm:pl-5">
605 <ResourceHistoryProcessNameCell
606 process={process}
607 visualDepth={
608 process.isServerRoot || shallowestChildDepth === null
609 ? 0
610 : Math.max(1, process.depth - shallowestChildDepth + 1)
611 }
612 />
613 </td>
614 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
615 {formatCpuTime(process.cpuSecondsApprox)}
616 </td>
617 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
618 {process.currentCpuPercent.toFixed(1)}%
619 </td>
620 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
621 {process.avgCpuPercent.toFixed(1)}%
622 </td>
623 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
624 {process.maxCpuPercent.toFixed(1)}%
625 </td>
626 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums">
627 {formatBytes(process.maxRssBytes)}
628 </td>
629 <td className="px-3 py-2 align-middle text-muted-foreground">
630 <Tooltip>
631 <TooltipTrigger
632 render={<span className="block truncate">{process.command}</span>}
633 />
634 <TooltipPopup side="top" variant="code">
635 {process.command}
636 </TooltipPopup>
637 </Tooltip>
638 </td>
639 <td className="px-3 py-2 text-right align-middle font-mono tabular-nums text-muted-foreground sm:pr-5">
640 {process.pid}
641 </td>
642 </tr>
643 ))}
644 </tbody>
645 </table>
646 </ScrollArea>
647 </div>
648 );
650​
651function DiagnosticsLastChecked({ checkedAt }: { checkedAt: DateTime.Utc | null }) {
652 const relative = useRelativeTimeState(checkedAt ? DateTime.formatIso(checkedAt) : null);
653​
654 if (relative.status === "missing") {
655 return <span className="text-2xs text-muted-foreground/50">Checking</span>;
656 }
657​
658 if (relative.status === "invalid") {
659 return <span className="text-2xs text-muted-foreground/50">Checked unavailable</span>;
660 }
661​
662 return (
663 <span className="text-2xs text-muted-foreground/60">
664 {relative.suffix ? (
665 <>
666 Checked <span className="font-mono tabular-nums">{relative.value}</span> {relative.suffix}
667 </>
668 ) : (
669 <>Checked {relative.value}</>
670 )}
671 </span>
672 );
674​
675function DiagnosticsRefreshButton({
676 isPending,
677 label,
678 onClick,
679}: {
680 isPending: boolean;
681 label: string;
682 onClick: () => void;
683}) {
684 return (
685 <Tooltip>
686 <TooltipTrigger
687 render={
688 <Button
689 size="icon-xs"
690 variant="ghost-muted"
691 disabled={isPending}
692 onClick={onClick}
693 aria-label={label}
694 >
695 <RefreshIcon refreshing={isPending} />
696 </Button>
697 }
698 />
699 <TooltipPopup side="top">{label}</TooltipPopup>
700 </Tooltip>
701 );
703​
704export function DiagnosticsSettingsPanel() {
705 const { environment } = useSettingsScope();
706 // Signals and pending UI state belong to one environment, so a scope switch starts fresh.
707 return <DiagnosticsSettingsBody key={environment?.environmentId ?? ""} />;
709​
710function DiagnosticsSettingsBody() {
711 const { environment } = useSettingsScope();
712 const environmentId = environment?.environmentId ?? null;
713 const canMaintainEnvironment = useEnvironmentScope(environmentId, AuthEnvironmentMaintainScope);
714 const observability = environment?.serverConfig?.observability;
715 const availableEditors = environment?.serverConfig?.availableEditors;
716 const canOpenHostEditor = useEnvironmentScope(environmentId, AuthOrchestrationOperateScope);
717 const session = useEnvironmentQuery(
718 environmentId === null ? null : environmentSession.sessionStateAtom(environmentId),
719 );
720 const diagnosticsAccess = resolveUsageAccess({
721 connectionPhase: environment?.connection.phase ?? "available",
722 session: session.data,
723 hasSessionError: session.error !== null,
724 });
725 const canReadDiagnostics = diagnosticsAccess.canReadDiagnostics;
726 const signalServerProcess = useAtomCommand(serverEnvironment.signalProcess, {
727 reportFailure: false,
728 });
729 const openInEditor = useOpenInPreferredEditor(environmentId, availableEditors ?? []);
730 const [resourceWindowMs, setResourceWindowMs] = useState(15 * 60_000);
731 const selectedResourceWindow =
732 RESOURCE_HISTORY_WINDOWS.find((option) => option.windowMs === resourceWindowMs) ??
733 RESOURCE_HISTORY_WINDOWS[1];
734 const { data, error, isPending, refresh } = useEnvironmentQuery(
735 environmentId === null || !canReadDiagnostics
736 ? null
737 : serverEnvironment.traceDiagnostics({ environmentId, input: {} }),
738 );
739 const {
740 data: processData,
741 error: processError,
742 isPending: isProcessPending,
743 refresh: refreshProcesses,
744 } = useEnvironmentQuery(
745 environmentId === null || !canReadDiagnostics
746 ? null
747 : serverEnvironment.processDiagnostics({ environmentId, input: {} }),
748 );
749 const {
750 data: resourceData,
751 error: resourceError,
752 isPending: isResourcePending,
753 refresh: refreshResources,
754 } = useEnvironmentQuery(
755 environmentId === null || !canReadDiagnostics
756 ? null
757 : serverEnvironment.processResourceHistory({
758 environmentId,
759 input: {
760 windowMs: selectedResourceWindow.windowMs,
761 bucketMs: selectedResourceWindow.bucketMs,
762 },
763 }),
764 );
765 const [isOpeningLogsDirectory, setIsOpeningLogsDirectory] = useState(false);
766 const [openLogsDirectoryError, setOpenLogsDirectoryError] = useState<string | null>(null);
767 const [signalingPid, setSignalingPid] = useState<number | null>(null);
768 const signalingPidRef = useRef<number | null>(null);
769​
770 const openLogsDirectory = () => {
771 const logsDirectoryPath = observability?.logsDirectoryPath ?? null;
772 if (!logsDirectoryPath) return;
773​
774 if (
775 environmentId === null ||
776 !readEnvironmentScope(environmentId, AuthOrchestrationOperateScope)
777 ) {
778 return;
779 }
780​
781 setIsOpeningLogsDirectory(true);
782 setOpenLogsDirectoryError(null);
783 void (async () => {
784 const result = await openInEditor(logsDirectoryPath);
785 setIsOpeningLogsDirectory(false);
786 if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
787 const error = squashAtomCommandFailure(result);
788 setOpenLogsDirectoryError(
789 error instanceof Error ? error.message : "Unable to open logs folder.",
790 );
791 }
792 })();
793 };
794​
795 const isInitialLoading = isPending && data === null;
796 const isProcessInitialLoading = isProcessPending && processData === null;
797 const isResourceInitialLoading = isResourcePending && resourceData === null;
798 // Wait for initial tables to stabilize search targets; later loads must preserve export drafts.
799 const [exportFormEnvironmentId, setExportFormEnvironmentId] =
800 useState<typeof environmentId>(null);
801 if (
802 exportFormEnvironmentId !== environmentId &&
803 canReadDiagnostics &&
804 !isInitialLoading &&
805 !isProcessInitialLoading &&
806 !isResourceInitialLoading
807 ) {
808 setExportFormEnvironmentId(environmentId);
809 }
810 const signalProcess = async (pid: number, signal: ServerProcessSignal) => {
811 const targetEnvironmentId = environmentId;
812 const process = processData?.processes.find((entry) => entry.pid === pid);
813 if (
814 targetEnvironmentId === null ||
815 process === undefined ||
816 !readEnvironmentScope(targetEnvironmentId, AuthEnvironmentMaintainScope)
817 )
818 return;
819 if (signalingPidRef.current !== null) return;
820 signalingPidRef.current = pid;
821 setSignalingPid(pid);
822 const clearSignaling = () => {
823 signalingPidRef.current = null;
824 setSignalingPid(null);
825 };
826 if (signal === "SIGKILL") {
827 let confirmed = false;
828 try {
829 confirmed = await ensureLocalApi().dialogs.confirm(
830 `Send SIGKILL to process ${pid}? This cannot be handled by the process.`,
831 { variant: "destructive" },
832 );
833 } catch (error) {
834 clearSignaling();
835 toastManager.add({
836 type: "error",
837 title: "Could not confirm signal",
838 description: error instanceof Error ? error.message : `Failed to send ${signal}.`,
839 });
840 return;
841 }
842 if (!confirmed) {
843 clearSignaling();
844 return;
845 }
846 }
847 // The server rejects a pid whose start time no longer matches, so a process that exited
848 // during confirmation is never signaled by mistake.
849 if (!readEnvironmentScope(targetEnvironmentId, AuthEnvironmentMaintainScope)) {
850 clearSignaling();
851 return;
852 }
853​
854 const result = await signalServerProcess({
855 environmentId: targetEnvironmentId,
856 input: { pid, startTimeMs: process.startTimeMs, signal },
857 });
858 clearSignaling();
859 if (result._tag === "Failure") {
860 if (!isAtomCommandInterrupted(result)) {
861 const error = squashAtomCommandFailure(result);
862 toastManager.add({
863 type: "error",
864 title: `Could not send ${signal}`,
865 description: error instanceof Error ? error.message : `Failed to send ${signal}.`,
866 });
867 }
868 return;
869 }
870 if (!result.value.signaled) {
871 const message = Option.getOrUndefined(result.value.message);
872 refreshProcesses();
873 if (isStaleProcessSignalMessage(message)) {
874 toastManager.add({
875 type: "info",
876 title: "Process already exited",
877 description: "The process is not a child of the T3 Server. It might already have exited.",
878 });
879 return;
880 }
881​
882 toastManager.add({
883 type: "error",
884 title: `Could not send ${signal}`,
885 description: message ?? `Failed to send ${signal}.`,
886 });
887 return;
888 }
889 refreshProcesses();
890 };
891​
892 const processDiagnosticsError = processData ? Option.getOrNull(processData.error) : null;
893 const processResourceError = resourceData ? Option.getOrNull(resourceData.error) : null;
894 const traceDiagnosticsError = data ? Option.getOrNull(data.error) : null;
895 const traceDiagnosticsPartialFailure = data
896 ? Option.getOrElse(data.partialFailure, () => false)
897 : false;
898​
899 if (!canReadDiagnostics) {
900 return (
901 <SettingsPageContainer>
902 <p className="text-sm text-muted-foreground">
903 {environmentId === null
904 ? "Connect an environment to see diagnostics."
905 : diagnosticsAccess.isPending
906 ? "Checking diagnostics access…"
907 : diagnosticsAccess.error}
908 </p>
909 </SettingsPageContainer>
910 );
911 }
912​
913 return (
914 <SettingsPageContainer width="expanded" className="gap-10">
915 <ResourceTelemetryDiagnostics environmentId={environmentId} />
916​
917 <SettingsSection
918 title="Live Processes"
919 headerAction={
920 <div className="flex items-center gap-1.5">
921 <DiagnosticsLastChecked checkedAt={processData?.readAt ?? null} />
922 <DiagnosticsRefreshButton
923 isPending={isProcessPending}
924 label="Refresh process diagnostics"
925 onClick={refreshProcesses}
926 />
927 </div>
928 }
929 >
930 <StatsGrid>
931 <StatBlock
932 label="Child Processes"
933 value={processData ? formatCount(processData.processCount) : "..."}
934 />
935 <StatBlock
936 label="CPU"
937 value={processData ? `${processData.totalCpuPercent.toFixed(1)}%` : "..."}
938 tooltip="Total CPU across live child processes of the current server process. The desktop shell and other parent processes are not included."
939 />
940 <StatBlock
941 label="Memory"
942 value={processData ? formatBytes(processData.totalRssBytes) : "..."}
943 tooltip="Total resident memory across live child processes of the current server process. The desktop shell and other parent processes are not included."
944 />
945 <StatBlock
946 label="Server PID"
947 value={processData ? String(processData.serverPid) : "..."}
948 />
949 </StatsGrid>
950 {processDiagnosticsError || processError ? (
951 <div className="space-y-2 border-t border-border/60 px-4 py-3 text-xs text-muted-foreground sm:px-5">
952 {processDiagnosticsError ? (
953 <div className="flex items-start gap-2 text-destructive">
954 <AlertTriangleIcon className="size-3.5 h-lh shrink-0" />
955 <span>{processDiagnosticsError.message}</span>
956 </div>
957 ) : null}
958 {processError ? (
959 <div className="flex items-start gap-2 text-destructive">
960 <AlertTriangleIcon className="size-3.5 h-lh shrink-0" />
961 <span>{processError}</span>
962 </div>
963 ) : null}
964 </div>
965 ) : null}
966 <ProcessDiagnosticsTable
967 processes={processData?.processes ?? []}
968 canMaintainEnvironment={canMaintainEnvironment}
969 signalingPid={signalingPid}
970 onSignal={signalProcess}
971 emptyLabel={
972 isProcessInitialLoading
973 ? "Loading live processes..."
974 : "No live descendant processes found."
975 }
976 />
977 </SettingsSection>
978​
979 <SettingsSection
980 title="Resource History"
981 headerAction={
982 <div className="flex items-center gap-1.5">
983 <ResourceHistoryWindowSelector
984 selectedWindowMs={resourceWindowMs}
985 onSelect={setResourceWindowMs}
986 />
987 <DiagnosticsLastChecked checkedAt={resourceData?.readAt ?? null} />
988 <DiagnosticsRefreshButton
989 isPending={isResourcePending}
990 label="Refresh resource history"
991 onClick={refreshResources}
992 />
993 </div>
994 }
995 >
996 <StatsGrid>
997 <StatBlock
998 label="CPU Time"
999 value={resourceData ? formatCpuTime(resourceData.totalCpuSecondsApprox) : "..."}
1000 tooltip="Approximate active CPU time for the T3 server root process and its descendants during the selected window. It grows only while sampled processes use CPU and older samples leave as the window moves."
1001 />
1002 <StatBlock
1003 label="Samples"
1004 value={resourceData ? formatCount(resourceData.retainedSampleCount) : "..."}
1005 tooltip="In-memory process samples retained by the server. The server samples only while diagnostics is open, and this resets when the server restarts."
1006 />
1007 <StatBlock
1008 label="Interval"
1009 value={
1010 resourceData
1011 ? resourceData.sampleIntervalMs === 0
1012 ? "On demand"
1013 : formatDiagnosticsDuration(resourceData.sampleIntervalMs)
1014 : "..."
1016 />
1017 <StatBlock
1018 label="Processes"
1019 value={resourceData ? formatCount(resourceData.topProcesses.length) : "..."}
1020 />
1021 </StatsGrid>
1022 {processResourceError || resourceError ? (
1023 <div className="space-y-2 border-t border-border/60 px-4 py-3 text-xs text-muted-foreground sm:px-5">
1024 {processResourceError ? (
1025 <div className="flex items-start gap-2 text-destructive">
1026 <AlertTriangleIcon className="size-3.5 h-lh shrink-0" />
1027 <span>{processResourceError.message}</span>
1028 </div>
1029 ) : null}
1030 {resourceError ? (
1031 <div className="flex items-start gap-2 text-destructive">
1032 <AlertTriangleIcon className="size-3.5 h-lh shrink-0" />
1033 <span>{resourceError}</span>
1034 </div>
1035 ) : null}
1036 </div>
1037 ) : null}
1038 <ProcessResourceHistoryChart buckets={resourceData?.buckets ?? []} />
1039 <ProcessResourceHistoryTable
1040 processes={resourceData?.topProcesses ?? []}
1041 emptyLabel={
1042 isResourceInitialLoading
1043 ? "Collecting process resource samples..."
1044 : "No process resource samples found for this window."
1046 />
1047 </SettingsSection>
1048​
1049 <SettingsSection
1050 title="Trace Diagnostics"
1051 headerAction={
1052 <div className="flex items-center gap-1.5">
1053 <DiagnosticsLastChecked checkedAt={data?.readAt ?? null} />
1054 <Tooltip>
1055 <TooltipTrigger
1056 render={
1057 <Button
1058 size="icon-xs"
1059 variant="ghost-muted"
1060 disabled={
1061 !canOpenHostEditor ||
1062 !observability?.logsDirectoryPath ||
1063 isOpeningLogsDirectory
1065 onClick={openLogsDirectory}
1066 aria-label="Open logs folder"
1068 <FolderOpenIcon />
1069 </Button>
1071 />
1072 <TooltipPopup side="top">Open logs folder</TooltipPopup>
1073 </Tooltip>
1074 <DiagnosticsRefreshButton
1075 isPending={isPending}
1076 label="Refresh trace diagnostics"
1077 onClick={refresh}
1078 />
1079 </div>
1082 <StatsGrid>
1083 <StatBlock label="Spans" value={data ? formatCount(data.recordCount) : "..."} />
1084 <StatBlock
1085 label="Failures"
1086 value={data ? formatCount(data.failureCount) : "..."}
1087 tone={data && data.failureCount > 0 ? "danger" : "default"}
1088 />
1089 <StatBlock
1090 label="Slow Spans"
1091 value={data ? formatCount(data.slowSpanCount) : "..."}
1092 tooltip={
1093 data
1094 ? `Spans with a duration of ${formatDiagnosticsDuration(data.slowSpanThresholdMs)} or longer.`
1095 : "Spans at or above the configured slow-span threshold."
1097 tone={data && data.slowSpanCount > 0 ? "warning" : "default"}
1098 />
1099 <StatBlock
1100 label="Parse Errors"
1101 value={data ? formatCount(data.parseErrorCount) : "..."}
1102 tone={data && data.parseErrorCount > 0 ? "warning" : "default"}
1103 />
1104 </StatsGrid>
1105 {openLogsDirectoryError || traceDiagnosticsError || error ? (
1106 <div className="space-y-2 border-t border-border/60 px-4 py-3 text-xs text-muted-foreground sm:px-5">
1107 {openLogsDirectoryError ? (
1108 <div className="flex items-start gap-2 text-destructive">
1109 <AlertTriangleIcon className="size-3.5 h-lh shrink-0" />
1110 <span>{openLogsDirectoryError}</span>
1111 </div>
1112 ) : null}
1113 {traceDiagnosticsError ? (
1114 <div
1115 className={cn(
1116 "flex items-start gap-2",
1117 traceDiagnosticsPartialFailure ? "text-warning-foreground" : "text-destructive",
1118 )}
1120 <AlertTriangleIcon className="size-3.5 h-lh shrink-0" />
1121 <span>
1122 {traceDiagnosticsPartialFailure
1123 ? `Some trace files could not be read, so diagnostics may be incomplete. ${traceDiagnosticsError.message}`
1124 : traceDiagnosticsError.message}
1125 </span>
1126 </div>
1127 ) : null}
1128 {error ? (
1129 <div className="flex items-start gap-2 text-destructive">
1130 <AlertTriangleIcon className="size-3.5 h-lh shrink-0" />
1131 <span>{error}</span>
1132 </div>
1133 ) : null}
1134 </div>
1135 ) : null}
1136 </SettingsSection>
1137​
1138 <SettingsSection title="Latest Failures">
1139 {data && data.latestFailures.length > 0 ? (
1140 <DiagnosticsTable headers={["Span", "Cause", "Duration", "Ended"]}>
1141 {data.latestFailures.map((failure) => (
1142 <tr key={`${failure.traceId}:${failure.spanId}`}>
1143 <td className="px-4 py-3 align-top text-xs font-medium text-foreground first:sm:pl-5">
1144 {failure.name}
1145 </td>
1146 <td className="max-w-[360px] px-4 py-3 align-top text-muted-foreground">
1147 <ExpandableText text={failure.cause} />
1148 </td>
1149 <td className="px-4 py-3 align-top font-mono tabular-nums">
1150 {formatDiagnosticsDuration(failure.durationMs)}
1151 </td>
1152 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums text-muted-foreground last:sm:pr-5">
1153 <RelativeTimeNoWrap value={failure.endedAt} />
1154 </td>
1155 </tr>
1156 ))}
1157 </DiagnosticsTable>
1158 ) : (
1159 <EmptyRows label={isInitialLoading ? "Loading failures..." : "No failed spans found."} />
1160 )}
1161 </SettingsSection>
1162​
1163 <SettingsSection title="Most Common Failures">
1164 {data && data.commonFailures.length > 0 ? (
1165 <DiagnosticsTable
1166 headers={["Span", "Count", "Cause", "Last Seen"]}
1167 minTableWidth="min-w-[760px]"
1169 {data.commonFailures.map((failure) => (
1170 <tr key={`${failure.name}:${failure.cause}`}>
1171 <td className="px-4 py-3 align-top text-xs font-medium text-foreground first:sm:pl-5">
1172 {failure.name}
1173 </td>
1174 <td className="px-4 py-3 align-top font-mono tabular-nums">
1175 {formatCount(failure.count)}
1176 </td>
1177 <td className="max-w-[360px] px-4 py-3 align-top text-muted-foreground">
1178 <ExpandableText text={failure.cause} />
1179 </td>
1180 <td className="w-px whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums text-muted-foreground last:sm:pr-5">
1181 <RelativeTimeNoWrap value={failure.lastSeenAt} />
1182 </td>
1183 </tr>
1184 ))}
1185 </DiagnosticsTable>
1186 ) : (
1187 <EmptyRows
1188 label={isInitialLoading ? "Loading failure groups..." : "No repeated failures found."}
1189 />
1190 )}
1191 </SettingsSection>
1192​
1193 <SettingsSection title="Slowest Spans">
1194 {data && data.slowestSpans.length > 0 ? (
1195 <DiagnosticsTable
1196 headers={["Span", "Duration", "Ended", "Trace"]}
1197 minTableWidth="min-w-[900px]"
1198 columnWidths={["w-[44%]", "w-[14%]", "w-[12%]", "w-[30%]"]}
1200 {data.slowestSpans.map((span) => (
1201 <tr key={`${span.traceId}:${span.spanId}`}>
1202 <td className="px-4 py-3 align-top text-xs font-medium text-foreground first:sm:pl-5">
1203 {span.name}
1204 </td>
1205 <td className="px-4 py-3 align-top font-mono tabular-nums">
1206 {formatDiagnosticsDuration(span.durationMs)}
1207 </td>
1208 <td className="w-px whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums text-muted-foreground">
1209 <RelativeTimeNoWrap value={span.endedAt} />
1210 </td>
1211 <td className="min-w-0 whitespace-nowrap px-4 py-3 align-top text-muted-foreground last:sm:pr-5">
1212 <TraceIdCell traceId={span.traceId} />
1213 </td>
1214 </tr>
1215 ))}
1216 </DiagnosticsTable>
1217 ) : (
1218 <EmptyRows label={isInitialLoading ? "Loading slow spans..." : "No spans found."} />
1219 )}
1220 </SettingsSection>
1221​
1222 <SettingsSection title="Span Logs">
1223 {data && data.latestWarningAndErrorLogs.length > 0 ? (
1224 <ScrollArea
1225 radius="none"
1226 chainVerticalScroll
1227 scrollFade
1228 hideScrollbars
1229 className="w-full max-w-full"
1231 <table className="w-full min-w-[920px] table-fixed text-left text-xs">
1232 <colgroup>
1233 <col className="w-[11%]" />
1234 <col className="w-[9%]" />
1235 <col className="w-[24%]" />
1236 <col className="w-[26%]" />
1237 <col className="w-[30%]" />
1238 </colgroup>
1239 <thead className="border-b border-border/60 text-2xs uppercase tracking-widest text-muted-foreground/70">
1240 <tr>
1241 <th className="whitespace-nowrap px-4 py-2.5 font-semibold sm:pl-5">Time</th>
1242 <th className="whitespace-nowrap px-4 py-2.5 font-semibold">Level</th>
1243 <th className="whitespace-nowrap px-4 py-2.5 font-semibold">Span</th>
1244 <th className="whitespace-nowrap px-4 py-2.5 font-semibold">Message</th>
1245 <th className="whitespace-nowrap px-4 py-2.5 font-semibold sm:pr-5">Trace</th>
1246 </tr>
1247 </thead>
1248 <tbody className="divide-y divide-border/60">
1249 {data.latestWarningAndErrorLogs.map((event) => (
1250 <tr
1251 key={`${event.traceId}:${event.spanId}:${DateTime.formatIso(event.seenAt)}:${event.message}`}
1252 className="hover:bg-muted/15"
1254 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums text-muted-foreground sm:pl-5">
1255 <RelativeTimeNoWrap value={event.seenAt} />
1256 </td>
1257 <td className="px-4 py-3 align-top">
1258 <span className="inline-flex rounded bg-muted px-1.5 py-0.5 font-mono text-2xs font-medium uppercase text-foreground/80">
1259 {event.level}
1260 </span>
1261 </td>
1262 <td className="px-4 py-3 align-top">
1263 <div className="truncate font-medium text-foreground">{event.spanName}</div>
1264 </td>
1265 <td className="px-4 py-3 align-top text-muted-foreground">
1266 <ExpandableText
1267 collapsedClassName="line-clamp-2"
1268 expandLabel="Show full message"
1269 text={event.message}
1270 />
1271 </td>
1272 <td className="min-w-0 whitespace-nowrap px-4 py-3 align-top text-muted-foreground sm:pr-5">
1273 <TraceIdCell traceId={event.traceId} />
1274 </td>
1275 </tr>
1276 ))}
1277 </tbody>
1278 </table>
1279 </ScrollArea>
1280 ) : (
1281 <EmptyRows
1282 label={isInitialLoading ? "Loading recent logs..." : "No warnings or errors found."}
1283 />
1284 )}
1285 </SettingsSection>
1286​
1287 <SettingsSection title="Top Span Names">
1288 {data && data.topSpansByCount.length > 0 ? (
1289 <DiagnosticsTable
1290 headers={["Span", "Count", "Failures", "Average", "Max"]}
1291 minTableWidth="min-w-[760px]"
1292 columnWidths={["w-[48%]", "w-[13%]", "w-[13%]", "w-[13%]", "w-[13%]"]}
1294 {data.topSpansByCount.map((span) => (
1295 <tr key={span.name}>
1296 <td className="px-4 py-3 align-top text-xs font-medium text-foreground first:sm:pl-5">
1297 {span.name}
1298 </td>
1299 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums">
1300 {formatCount(span.count)}
1301 </td>
1302 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums">
1303 {formatCount(span.failureCount)}
1304 </td>
1305 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums">
1306 {formatDiagnosticsDuration(span.averageDurationMs)}
1307 </td>
1308 <td className="whitespace-nowrap px-4 py-3 align-top font-mono tabular-nums last:sm:pr-5">
1309 {formatDiagnosticsDuration(span.maxDurationMs)}
1310 </td>
1311 </tr>
1312 ))}
1313 </DiagnosticsTable>
1314 ) : (
1315 <EmptyRows label={isInitialLoading ? "Loading span names..." : "No spans found."} />
1316 )}
1317 </SettingsSection>
1318 {exportFormEnvironmentId === environmentId && <TelemetryExportSettings />}
1319 </SettingsPageContainer>
1320 );