proteum 2.5.9 → 2.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -4
- package/agents/project/AGENTS.md +131 -91
- package/agents/project/CODING_STYLE.md +42 -40
- package/agents/project/client/AGENTS.md +0 -1
- package/agents/project/diagnostics.md +6 -5
- package/agents/project/optimizations.md +1 -7
- package/agents/project/server/services/AGENTS.md +1 -3
- package/agents/project/tests/AGENTS.md +3 -3
- package/cli/commands/session.ts +36 -5
- package/cli/commands/verify.ts +6 -1
- package/cli/compiler/client/index.ts +11 -4
- package/cli/compiler/common/uiSingletons.ts +76 -0
- package/cli/compiler/server/index.ts +34 -12
- package/cli/presentation/commands.ts +6 -1
- package/cli/runtime/commands.ts +2 -0
- package/cli/scaffold/index.ts +3 -0
- package/cli/scaffold/templates.ts +62 -6
- package/cli/utils/agents.ts +2 -2
- package/client/dev/profiler/index.tsx +761 -455
- package/common/dev/session.ts +32 -0
- package/common/errors/index.tsx +0 -1
- package/docs/agent-routing.md +2 -2
- package/package.json +1 -1
- package/server/app/container/console/index.ts +0 -17
- package/server/services/router/http/index.ts +130 -33
- package/tests/agents-utils.test.cjs +0 -4
- package/tests/dev-session-login-url.test.cjs +33 -0
- package/tests/ui-singletons.test.cjs +104 -0
- package/agents/project/app-root/AGENTS.md +0 -14
- package/agents/project/root/AGENTS.md +0 -399
|
@@ -142,19 +142,36 @@ const profilerStyles = `
|
|
|
142
142
|
bottom: calc(10px + env(safe-area-inset-bottom, 0px));
|
|
143
143
|
display: inline-flex;
|
|
144
144
|
align-items: center;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
justify-content: center;
|
|
146
|
+
width: 46px;
|
|
147
|
+
height: 46px;
|
|
148
|
+
padding: 0;
|
|
148
149
|
border: 1px solid var(--profiler-line-strong);
|
|
149
|
-
border-radius:
|
|
150
|
+
border-radius: 999px;
|
|
150
151
|
background: var(--profiler-bg-strong);
|
|
151
152
|
backdrop-filter: none;
|
|
152
153
|
color: var(--profiler-brand);
|
|
153
|
-
box-shadow:
|
|
154
|
-
cursor:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
box-shadow: 0 8px 26px rgba(19, 32, 51, 0.16);
|
|
155
|
+
cursor: grab;
|
|
156
|
+
line-height: 0;
|
|
157
|
+
touch-action: none;
|
|
158
|
+
user-select: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.proteum-profiler__handle:hover {
|
|
162
|
+
color: var(--profiler-text);
|
|
163
|
+
background: var(--profiler-surface-hover);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.proteum-profiler__handle:active,
|
|
167
|
+
.proteum-profiler__handle--dragging {
|
|
168
|
+
cursor: grabbing;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.proteum-profiler__handleIcon {
|
|
172
|
+
width: 25px;
|
|
173
|
+
height: 25px;
|
|
174
|
+
pointer-events: none;
|
|
158
175
|
}
|
|
159
176
|
|
|
160
177
|
.proteum-profiler__panel {
|
|
@@ -514,16 +531,6 @@ const profilerStyles = `
|
|
|
514
531
|
color: var(--profiler-muted);
|
|
515
532
|
}
|
|
516
533
|
|
|
517
|
-
.proteum-profiler__requestWorkspace {
|
|
518
|
-
display: grid;
|
|
519
|
-
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
|
|
520
|
-
gap: 0;
|
|
521
|
-
align-items: stretch;
|
|
522
|
-
min-height: 100%;
|
|
523
|
-
height: 100%;
|
|
524
|
-
max-height: 100%;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
534
|
.proteum-profiler__splitView {
|
|
528
535
|
display: grid;
|
|
529
536
|
grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
|
|
@@ -604,6 +611,12 @@ const profilerStyles = `
|
|
|
604
611
|
scrollbar-width: thin;
|
|
605
612
|
}
|
|
606
613
|
|
|
614
|
+
.proteum-profiler__sidebarCharts {
|
|
615
|
+
display: grid;
|
|
616
|
+
gap: 0;
|
|
617
|
+
min-width: 0;
|
|
618
|
+
}
|
|
619
|
+
|
|
607
620
|
.proteum-profiler__titleRow {
|
|
608
621
|
padding: 8px 10px;
|
|
609
622
|
background: var(--profiler-title-row-bg);
|
|
@@ -679,13 +692,6 @@ const profilerStyles = `
|
|
|
679
692
|
background: transparent !important;
|
|
680
693
|
}
|
|
681
694
|
|
|
682
|
-
.proteum-profiler__chartGrid {
|
|
683
|
-
display: grid;
|
|
684
|
-
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
685
|
-
gap: 0;
|
|
686
|
-
border-top: 1px solid var(--profiler-line);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
695
|
.proteum-profiler__chartCard {
|
|
690
696
|
display: grid;
|
|
691
697
|
align-content: start;
|
|
@@ -698,6 +704,10 @@ const profilerStyles = `
|
|
|
698
704
|
border-left: none;
|
|
699
705
|
}
|
|
700
706
|
|
|
707
|
+
.proteum-profiler__sidebarCharts > .proteum-profiler__chartCard {
|
|
708
|
+
border-left: none;
|
|
709
|
+
}
|
|
710
|
+
|
|
701
711
|
.proteum-profiler__chartHeader {
|
|
702
712
|
display: grid;
|
|
703
713
|
gap: 4px;
|
|
@@ -771,10 +781,6 @@ const profilerStyles = `
|
|
|
771
781
|
overflow: auto;
|
|
772
782
|
}
|
|
773
783
|
|
|
774
|
-
.proteum-profiler__chartGrid {
|
|
775
|
-
grid-template-columns: 1fr;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
784
|
.proteum-profiler__chartCard {
|
|
779
785
|
border-left: none;
|
|
780
786
|
}
|
|
@@ -787,13 +793,6 @@ const profilerStyles = `
|
|
|
787
793
|
min-width: 132px;
|
|
788
794
|
}
|
|
789
795
|
|
|
790
|
-
.proteum-profiler__requestWorkspace {
|
|
791
|
-
grid-template-columns: 1fr;
|
|
792
|
-
min-height: 0;
|
|
793
|
-
height: auto;
|
|
794
|
-
max-height: none;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
796
|
.proteum-profiler__splitView {
|
|
798
797
|
grid-template-columns: 1fr;
|
|
799
798
|
min-height: 0;
|
|
@@ -1622,7 +1621,7 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1622
1621
|
tags: [trace.status, ...(trace.requestId ? [`request:${trace.requestId}`] : [])],
|
|
1623
1622
|
}));
|
|
1624
1623
|
const requestItems = [...syncItems, ...asyncItems];
|
|
1625
|
-
const [selectedRequestId, setSelectedRequestId] = React.useState<string | undefined>(
|
|
1624
|
+
const [selectedRequestId, setSelectedRequestId] = React.useState<string | undefined>();
|
|
1626
1625
|
const endpointDurationChart = buildHorizontalBarChartOptions({
|
|
1627
1626
|
color: profilerChartTheme.blue,
|
|
1628
1627
|
entries: buildDurationEntries(
|
|
@@ -1652,103 +1651,108 @@ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1652
1651
|
});
|
|
1653
1652
|
|
|
1654
1653
|
React.useEffect(() => {
|
|
1655
|
-
if (requestItems.some((item) => item.id === selectedRequestId)) return;
|
|
1656
|
-
setSelectedRequestId(
|
|
1654
|
+
if (!selectedRequestId || requestItems.some((item) => item.id === selectedRequestId)) return;
|
|
1655
|
+
setSelectedRequestId(undefined);
|
|
1657
1656
|
}, [requestItems, selectedRequestId]);
|
|
1658
1657
|
|
|
1658
|
+
const toggleRequestSelection = React.useCallback((requestId: string) => {
|
|
1659
|
+
setSelectedRequestId((currentRequestId: string | undefined) => (currentRequestId === requestId ? undefined : requestId));
|
|
1660
|
+
}, []);
|
|
1661
|
+
|
|
1659
1662
|
const waterfallItems = buildApiWaterfallItems(requestItems);
|
|
1660
|
-
const selectedItem = requestItems.find((item) => item.id === selectedRequestId)
|
|
1663
|
+
const selectedItem = selectedRequestId ? requestItems.find((item) => item.id === selectedRequestId) : undefined;
|
|
1661
1664
|
|
|
1662
1665
|
return (
|
|
1663
|
-
<
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
<
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1666
|
+
<ProfilerSplitLayout
|
|
1667
|
+
sidebar={
|
|
1668
|
+
selectedItem ? (
|
|
1669
|
+
<ApiRequestSidebar item={selectedItem} />
|
|
1670
|
+
) : (
|
|
1671
|
+
<ChartSidebar eyebrow="API charts" title="API overview">
|
|
1672
|
+
<ChartSection
|
|
1673
|
+
emptyLabel="No API request timings were captured for this session."
|
|
1674
|
+
options={endpointDurationChart}
|
|
1675
|
+
subtitle="Rank the most expensive endpoints across synchronous and async requests."
|
|
1676
|
+
title="Hot Endpoints"
|
|
1677
|
+
/>
|
|
1678
|
+
<ChartSection
|
|
1679
|
+
emptyLabel="No API origin timings were captured for this session."
|
|
1680
|
+
options={originWorkloadChart}
|
|
1681
|
+
subtitle="Compare time spent in SSR fetchers, batch fetchers, and client async calls."
|
|
1682
|
+
title="Origin Mix"
|
|
1683
|
+
/>
|
|
1684
|
+
<ChartSection
|
|
1685
|
+
emptyLabel="No API status information was captured for this session."
|
|
1686
|
+
options={statusCountChart}
|
|
1687
|
+
subtitle="Spot failures or pending requests without scanning every row."
|
|
1688
|
+
title="Status Spread"
|
|
1689
|
+
/>
|
|
1690
|
+
<WaterfallChart
|
|
1691
|
+
emptyLabel="No API requests were captured for this session."
|
|
1692
|
+
itemLabel="request"
|
|
1693
|
+
items={waterfallItems}
|
|
1694
|
+
onSelect={(requestId) => setSelectedRequestId(requestId)}
|
|
1695
|
+
/>
|
|
1696
|
+
</ChartSidebar>
|
|
1697
|
+
)
|
|
1698
|
+
}
|
|
1699
|
+
>
|
|
1700
|
+
<div className="proteum-profiler__requestGroups">
|
|
1701
|
+
<div className="proteum-profiler__requestGroup">
|
|
1702
|
+
<div className="proteum-profiler__requestGroupHeader">
|
|
1703
|
+
<div className="proteum-profiler__sectionTitle">Synchronous calls</div>
|
|
1704
|
+
<div className="proteum-profiler__requestGroupCount">
|
|
1705
|
+
{syncItems.length} item{syncItems.length === 1 ? '' : 's'}
|
|
1700
1706
|
</div>
|
|
1701
|
-
|
|
1702
|
-
{syncItems.length === 0 ? (
|
|
1703
|
-
<div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
|
|
1704
|
-
) : (
|
|
1705
|
-
<div className="proteum-profiler__list">
|
|
1706
|
-
{syncItems.map((item) => (
|
|
1707
|
-
<ApiRequestListEntry
|
|
1708
|
-
isSelected={item.id === selectedItem?.id}
|
|
1709
|
-
item={item}
|
|
1710
|
-
key={item.id}
|
|
1711
|
-
onSelect={() => setSelectedRequestId(item.id)}
|
|
1712
|
-
/>
|
|
1713
|
-
))}
|
|
1714
|
-
</div>
|
|
1715
|
-
)}
|
|
1716
1707
|
</div>
|
|
1717
1708
|
|
|
1718
|
-
|
|
1719
|
-
<div className="proteum-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1709
|
+
{syncItems.length === 0 ? (
|
|
1710
|
+
<div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
|
|
1711
|
+
) : (
|
|
1712
|
+
<div className="proteum-profiler__list">
|
|
1713
|
+
{syncItems.map((item) => (
|
|
1714
|
+
<ApiRequestListEntry
|
|
1715
|
+
isSelected={item.id === selectedRequestId}
|
|
1716
|
+
item={item}
|
|
1717
|
+
key={item.id}
|
|
1718
|
+
onSelect={() => toggleRequestSelection(item.id)}
|
|
1719
|
+
/>
|
|
1720
|
+
))}
|
|
1724
1721
|
</div>
|
|
1722
|
+
)}
|
|
1723
|
+
</div>
|
|
1725
1724
|
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
isSelected={item.id === selectedItem?.id}
|
|
1733
|
-
item={item}
|
|
1734
|
-
key={item.id}
|
|
1735
|
-
onSelect={() => setSelectedRequestId(item.id)}
|
|
1736
|
-
/>
|
|
1737
|
-
))}
|
|
1738
|
-
</div>
|
|
1739
|
-
)}
|
|
1725
|
+
<div className="proteum-profiler__requestGroup">
|
|
1726
|
+
<div className="proteum-profiler__requestGroupHeader">
|
|
1727
|
+
<div className="proteum-profiler__sectionTitle">Async requests</div>
|
|
1728
|
+
<div className="proteum-profiler__requestGroupCount">
|
|
1729
|
+
{asyncItems.length} item{asyncItems.length === 1 ? '' : 's'}
|
|
1730
|
+
</div>
|
|
1740
1731
|
</div>
|
|
1732
|
+
|
|
1733
|
+
{asyncItems.length === 0 ? (
|
|
1734
|
+
<div className="proteum-profiler__empty">No async API calls captured.</div>
|
|
1735
|
+
) : (
|
|
1736
|
+
<div className="proteum-profiler__list">
|
|
1737
|
+
{asyncItems.map((item) => (
|
|
1738
|
+
<ApiRequestListEntry
|
|
1739
|
+
isSelected={item.id === selectedRequestId}
|
|
1740
|
+
item={item}
|
|
1741
|
+
key={item.id}
|
|
1742
|
+
onSelect={() => toggleRequestSelection(item.id)}
|
|
1743
|
+
/>
|
|
1744
|
+
))}
|
|
1745
|
+
</div>
|
|
1746
|
+
)}
|
|
1741
1747
|
</div>
|
|
1742
1748
|
</div>
|
|
1743
|
-
|
|
1744
|
-
<ApiRequestSidebar item={selectedItem} />
|
|
1745
|
-
</div>
|
|
1749
|
+
</ProfilerSplitLayout>
|
|
1746
1750
|
);
|
|
1747
1751
|
};
|
|
1748
1752
|
|
|
1749
1753
|
const SqlPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
1750
1754
|
const { groups, queryItems } = buildSqlQueryWorkspace(session);
|
|
1751
|
-
const [selectedQueryId, setSelectedQueryId] = React.useState<string | undefined>(
|
|
1755
|
+
const [selectedQueryId, setSelectedQueryId] = React.useState<string | undefined>();
|
|
1752
1756
|
const callerDurationChart = buildHorizontalBarChartOptions({
|
|
1753
1757
|
color: profilerChartTheme.teal,
|
|
1754
1758
|
entries: buildDurationEntries(queryItems, (item) => item.callerLabel, (item) => item.durationMs),
|
|
@@ -1781,75 +1785,80 @@ const SqlPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
1781
1785
|
});
|
|
1782
1786
|
|
|
1783
1787
|
React.useEffect(() => {
|
|
1784
|
-
if (queryItems.some((item) => item.id === selectedQueryId)) return;
|
|
1785
|
-
setSelectedQueryId(
|
|
1788
|
+
if (!selectedQueryId || queryItems.some((item) => item.id === selectedQueryId)) return;
|
|
1789
|
+
setSelectedQueryId(undefined);
|
|
1786
1790
|
}, [queryItems, selectedQueryId]);
|
|
1787
1791
|
|
|
1792
|
+
const toggleQuerySelection = React.useCallback((queryId: string) => {
|
|
1793
|
+
setSelectedQueryId((currentQueryId: string | undefined) => (currentQueryId === queryId ? undefined : queryId));
|
|
1794
|
+
}, []);
|
|
1795
|
+
|
|
1788
1796
|
const waterfallItems = buildSqlWaterfallItems(queryItems);
|
|
1789
|
-
const selectedItem = queryItems.find((item) => item.id === selectedQueryId)
|
|
1797
|
+
const selectedItem = selectedQueryId ? queryItems.find((item) => item.id === selectedQueryId) : undefined;
|
|
1790
1798
|
|
|
1791
1799
|
return (
|
|
1792
|
-
<
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
<
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1800
|
+
<ProfilerSplitLayout
|
|
1801
|
+
sidebar={
|
|
1802
|
+
selectedItem ? (
|
|
1803
|
+
<SqlQuerySidebar item={selectedItem} />
|
|
1804
|
+
) : (
|
|
1805
|
+
<ChartSidebar eyebrow="SQL charts" title="SQL overview">
|
|
1806
|
+
<ChartSection
|
|
1807
|
+
emptyLabel="No SQL timings were captured for this session."
|
|
1808
|
+
options={callerDurationChart}
|
|
1809
|
+
subtitle="Show which callers are driving the most database time."
|
|
1810
|
+
title="Hot Callers"
|
|
1811
|
+
/>
|
|
1812
|
+
<ChartSection
|
|
1813
|
+
emptyLabel="No SQL operation counts were captured for this session."
|
|
1814
|
+
options={operationCountChart}
|
|
1815
|
+
subtitle="Highlight whether reads, writes, or raw queries dominate the request."
|
|
1816
|
+
title="Operation Mix"
|
|
1817
|
+
/>
|
|
1818
|
+
<ChartSection
|
|
1819
|
+
emptyLabel="No caller or operation overlap was captured for this session."
|
|
1820
|
+
options={callerOperationHeatmap}
|
|
1821
|
+
subtitle="Surface dense caller and operation combinations at a glance."
|
|
1822
|
+
title="Caller Heatmap"
|
|
1823
|
+
/>
|
|
1824
|
+
<WaterfallChart
|
|
1825
|
+
emptyLabel="No SQL queries were captured for this session."
|
|
1826
|
+
itemLabel="query"
|
|
1827
|
+
items={waterfallItems}
|
|
1828
|
+
onSelect={(queryId) => setSelectedQueryId(queryId)}
|
|
1829
|
+
/>
|
|
1830
|
+
</ChartSidebar>
|
|
1831
|
+
)
|
|
1832
|
+
}
|
|
1833
|
+
>
|
|
1834
|
+
<div className="proteum-profiler__requestGroups">
|
|
1835
|
+
{groups.length === 0 ? (
|
|
1836
|
+
<div className="proteum-profiler__empty">No SQL queries were captured for this session.</div>
|
|
1837
|
+
) : (
|
|
1838
|
+
groups.map((group) => (
|
|
1839
|
+
<div className="proteum-profiler__requestGroup" key={group.id}>
|
|
1840
|
+
<div className="proteum-profiler__requestGroupHeader">
|
|
1841
|
+
<div className="proteum-profiler__sectionTitle">{group.label}</div>
|
|
1842
|
+
<div className="proteum-profiler__requestGroupCount">
|
|
1843
|
+
{group.items.length} item{group.items.length === 1 ? '' : 's'}
|
|
1833
1844
|
</div>
|
|
1845
|
+
</div>
|
|
1834
1846
|
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
</div>
|
|
1847
|
+
<div className="proteum-profiler__list">
|
|
1848
|
+
{group.items.map((item) => (
|
|
1849
|
+
<SqlQueryListEntry
|
|
1850
|
+
isSelected={item.id === selectedQueryId}
|
|
1851
|
+
item={item}
|
|
1852
|
+
key={item.id}
|
|
1853
|
+
onSelect={() => toggleQuerySelection(item.id)}
|
|
1854
|
+
/>
|
|
1855
|
+
))}
|
|
1845
1856
|
</div>
|
|
1846
|
-
|
|
1847
|
-
)
|
|
1848
|
-
|
|
1857
|
+
</div>
|
|
1858
|
+
))
|
|
1859
|
+
)}
|
|
1849
1860
|
</div>
|
|
1850
|
-
|
|
1851
|
-
<SqlQuerySidebar item={selectedItem} />
|
|
1852
|
-
</div>
|
|
1861
|
+
</ProfilerSplitLayout>
|
|
1853
1862
|
);
|
|
1854
1863
|
};
|
|
1855
1864
|
|
|
@@ -3154,6 +3163,35 @@ const ChartSection = ({
|
|
|
3154
3163
|
</div>
|
|
3155
3164
|
);
|
|
3156
3165
|
|
|
3166
|
+
const ChartSidebar = ({
|
|
3167
|
+
children,
|
|
3168
|
+
eyebrow = 'Charts',
|
|
3169
|
+
title = 'Profiler overview',
|
|
3170
|
+
}: {
|
|
3171
|
+
children: React.ReactNode;
|
|
3172
|
+
eyebrow?: string;
|
|
3173
|
+
title?: string;
|
|
3174
|
+
}) => (
|
|
3175
|
+
<aside className="proteum-profiler__sidebar proteum-profiler__sidebar--charts">
|
|
3176
|
+
<div className="proteum-profiler__sidebarScroller">
|
|
3177
|
+
<div className="proteum-profiler__sidebarHeader">
|
|
3178
|
+
<div className="proteum-profiler__sidebarEyebrow">{eyebrow}</div>
|
|
3179
|
+
<div className="proteum-profiler__sidebarTitle">
|
|
3180
|
+
<strong>{title}</strong>
|
|
3181
|
+
</div>
|
|
3182
|
+
</div>
|
|
3183
|
+
<div className="proteum-profiler__sidebarCharts">{children}</div>
|
|
3184
|
+
</div>
|
|
3185
|
+
</aside>
|
|
3186
|
+
);
|
|
3187
|
+
|
|
3188
|
+
const ProfilerSplitLayout = ({ children, sidebar }: { children: React.ReactNode; sidebar: React.ReactNode }) => (
|
|
3189
|
+
<div className="proteum-profiler__splitView">
|
|
3190
|
+
<div className="proteum-profiler__splitColumn">{children}</div>
|
|
3191
|
+
{sidebar}
|
|
3192
|
+
</div>
|
|
3193
|
+
);
|
|
3194
|
+
|
|
3157
3195
|
const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
3158
3196
|
const selections: TTraceEventInspectorSelection[] = session.traces.flatMap((traceItem) =>
|
|
3159
3197
|
traceItem.trace
|
|
@@ -3165,80 +3203,88 @@ const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) =>
|
|
|
3165
3203
|
}))
|
|
3166
3204
|
: [],
|
|
3167
3205
|
);
|
|
3168
|
-
const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(
|
|
3206
|
+
const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>();
|
|
3169
3207
|
|
|
3170
3208
|
React.useEffect(() => {
|
|
3171
|
-
if (selections.some((selection) => selection.key === selectedEventKey)) return;
|
|
3172
|
-
setSelectedEventKey(
|
|
3209
|
+
if (!selectedEventKey || selections.some((selection) => selection.key === selectedEventKey)) return;
|
|
3210
|
+
setSelectedEventKey(undefined);
|
|
3173
3211
|
}, [selectedEventKey, selections]);
|
|
3174
3212
|
|
|
3213
|
+
const toggleEventSelection = React.useCallback((eventKey: string) => {
|
|
3214
|
+
setSelectedEventKey((currentEventKey: string | undefined) => (currentEventKey === eventKey ? undefined : eventKey));
|
|
3215
|
+
}, []);
|
|
3216
|
+
|
|
3175
3217
|
const waterfallItems = buildTimelineWaterfallItems(session);
|
|
3176
|
-
const selected = selections.find((selection) => selection.key === selectedEventKey)
|
|
3218
|
+
const selected = selectedEventKey ? selections.find((selection) => selection.key === selectedEventKey) : undefined;
|
|
3177
3219
|
|
|
3178
3220
|
return (
|
|
3179
|
-
<
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
<div className="proteum-profiler__sectionTitle">Navigation steps</div>
|
|
3191
|
-
</div>
|
|
3192
|
-
<div className="proteum-profiler__list">
|
|
3193
|
-
{session.steps.map((step) => (
|
|
3194
|
-
<div className="proteum-profiler__row" key={step.id}>
|
|
3195
|
-
<div className="proteum-profiler__rowHeader">
|
|
3196
|
-
<strong>{step.label}</strong>
|
|
3197
|
-
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
|
|
3198
|
-
</div>
|
|
3199
|
-
<div className="proteum-profiler__tags">
|
|
3200
|
-
<span className="proteum-profiler__tag">{step.status}</span>
|
|
3201
|
-
{Object.entries(step.details || {}).map(([key, value]) => (
|
|
3202
|
-
<span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
|
|
3203
|
-
{key}:{String(value)}
|
|
3204
|
-
</span>
|
|
3205
|
-
))}
|
|
3206
|
-
{step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
|
|
3207
|
-
</div>
|
|
3208
|
-
</div>
|
|
3209
|
-
))}
|
|
3210
|
-
</div>
|
|
3211
|
-
</div>
|
|
3212
|
-
|
|
3213
|
-
{session.traces.map((traceItem) =>
|
|
3214
|
-
traceItem.trace ? (
|
|
3215
|
-
<TraceRows
|
|
3216
|
-
key={traceItem.id}
|
|
3217
|
-
onSelect={setSelectedEventKey}
|
|
3218
|
-
selectedEventKey={selectedEventKey}
|
|
3219
|
-
trace={traceItem.trace}
|
|
3221
|
+
<ProfilerSplitLayout
|
|
3222
|
+
sidebar={
|
|
3223
|
+
selected ? (
|
|
3224
|
+
<TraceEventSidebar event={selected.event} label={selected.label} trace={selected.trace} />
|
|
3225
|
+
) : (
|
|
3226
|
+
<ChartSidebar eyebrow="Timeline charts" title="Timeline overview">
|
|
3227
|
+
<WaterfallChart
|
|
3228
|
+
emptyLabel="No timeline events were captured for this session."
|
|
3229
|
+
itemLabel="event"
|
|
3230
|
+
items={waterfallItems}
|
|
3231
|
+
onSelect={(eventKey) => setSelectedEventKey(eventKey)}
|
|
3220
3232
|
/>
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3233
|
+
</ChartSidebar>
|
|
3234
|
+
)
|
|
3235
|
+
}
|
|
3236
|
+
>
|
|
3237
|
+
<div className="proteum-profiler__section">
|
|
3238
|
+
<div className="proteum-profiler__titleRow">
|
|
3239
|
+
<div className="proteum-profiler__sectionTitle">Navigation steps</div>
|
|
3240
|
+
</div>
|
|
3241
|
+
<div className="proteum-profiler__list">
|
|
3242
|
+
{session.steps.map((step) => (
|
|
3243
|
+
<div className="proteum-profiler__row" key={step.id}>
|
|
3244
|
+
<div className="proteum-profiler__rowHeader">
|
|
3245
|
+
<strong>{step.label}</strong>
|
|
3246
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
|
|
3226
3247
|
</div>
|
|
3227
|
-
<div className="proteum-
|
|
3228
|
-
{
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3248
|
+
<div className="proteum-profiler__tags">
|
|
3249
|
+
<span className="proteum-profiler__tag">{step.status}</span>
|
|
3250
|
+
{Object.entries(step.details || {}).map(([key, value]) => (
|
|
3251
|
+
<span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
|
|
3252
|
+
{key}:{String(value)}
|
|
3253
|
+
</span>
|
|
3254
|
+
))}
|
|
3255
|
+
{step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
|
|
3234
3256
|
</div>
|
|
3235
3257
|
</div>
|
|
3236
|
-
)
|
|
3237
|
-
|
|
3258
|
+
))}
|
|
3259
|
+
</div>
|
|
3238
3260
|
</div>
|
|
3239
3261
|
|
|
3240
|
-
|
|
3241
|
-
|
|
3262
|
+
{session.traces.map((traceItem) =>
|
|
3263
|
+
traceItem.trace ? (
|
|
3264
|
+
<TraceRows
|
|
3265
|
+
key={traceItem.id}
|
|
3266
|
+
onSelect={toggleEventSelection}
|
|
3267
|
+
selectedEventKey={selectedEventKey}
|
|
3268
|
+
trace={traceItem.trace}
|
|
3269
|
+
/>
|
|
3270
|
+
) : (
|
|
3271
|
+
<div className="proteum-profiler__row" key={traceItem.id}>
|
|
3272
|
+
<div className="proteum-profiler__rowHeader">
|
|
3273
|
+
<strong>{formatSessionTraceDisplay(traceItem)}</strong>
|
|
3274
|
+
<span className="proteum-profiler__mono proteum-profiler__muted">{traceItem.status}</span>
|
|
3275
|
+
</div>
|
|
3276
|
+
<div className="proteum-profiler__mono">
|
|
3277
|
+
{formatProfilerRequestReference({
|
|
3278
|
+
fallbackLabel: traceItem.label,
|
|
3279
|
+
method: traceItem.method,
|
|
3280
|
+
path: traceItem.path,
|
|
3281
|
+
requestData: getTraceRequestData(traceItem.trace),
|
|
3282
|
+
})}
|
|
3283
|
+
</div>
|
|
3284
|
+
</div>
|
|
3285
|
+
),
|
|
3286
|
+
)}
|
|
3287
|
+
</ProfilerSplitLayout>
|
|
3242
3288
|
);
|
|
3243
3289
|
};
|
|
3244
3290
|
|
|
@@ -3297,55 +3343,61 @@ const AuthPanel = ({ session }: { session: TProfilerNavigationSession }) => {
|
|
|
3297
3343
|
trace: section.trace,
|
|
3298
3344
|
})),
|
|
3299
3345
|
);
|
|
3300
|
-
const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(
|
|
3346
|
+
const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>();
|
|
3301
3347
|
|
|
3302
3348
|
React.useEffect(() => {
|
|
3303
|
-
if (selections.some((selection) => selection.key === selectedEventKey)) return;
|
|
3304
|
-
setSelectedEventKey(
|
|
3349
|
+
if (!selectedEventKey || selections.some((selection) => selection.key === selectedEventKey)) return;
|
|
3350
|
+
setSelectedEventKey(undefined);
|
|
3305
3351
|
}, [selectedEventKey, selections]);
|
|
3306
3352
|
|
|
3353
|
+
const toggleEventSelection = React.useCallback((eventKey: string) => {
|
|
3354
|
+
setSelectedEventKey((currentEventKey: string | undefined) => (currentEventKey === eventKey ? undefined : eventKey));
|
|
3355
|
+
}, []);
|
|
3356
|
+
|
|
3307
3357
|
if (authSections.length === 0) return <div className="proteum-profiler__empty">No auth activity was captured for this session.</div>;
|
|
3308
3358
|
|
|
3309
|
-
const selected = selections.find((selection) => selection.key === selectedEventKey)
|
|
3359
|
+
const selected = selectedEventKey ? selections.find((selection) => selection.key === selectedEventKey) : undefined;
|
|
3310
3360
|
|
|
3311
3361
|
return (
|
|
3312
|
-
<
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
<
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3362
|
+
<ProfilerSplitLayout
|
|
3363
|
+
sidebar={
|
|
3364
|
+
selected ? (
|
|
3365
|
+
<TraceEventSidebar event={selected.event} label={selected.label} trace={selected.trace} />
|
|
3366
|
+
) : (
|
|
3367
|
+
<ChartSidebar eyebrow="Auth charts" title="Auth overview">
|
|
3368
|
+
<ChartSection
|
|
3369
|
+
emptyLabel="No auth events were captured for this session."
|
|
3370
|
+
options={authEventTypeChart}
|
|
3371
|
+
subtitle="See which auth phases are actually active for the selected navigation."
|
|
3372
|
+
title="Auth Flow"
|
|
3373
|
+
/>
|
|
3374
|
+
<ChartSection
|
|
3375
|
+
emptyLabel="No rule checks were captured for this session."
|
|
3376
|
+
options={authRuleChart}
|
|
3377
|
+
subtitle="Highlight the rules that are firing most often in the current auth flow."
|
|
3378
|
+
title="Rule Pressure"
|
|
3379
|
+
/>
|
|
3380
|
+
<ChartSection
|
|
3381
|
+
emptyLabel="No auth outcome events were captured for this session."
|
|
3382
|
+
options={authResultChart}
|
|
3383
|
+
subtitle="Summarize allow, deny, and routing outcomes without reading every trace row."
|
|
3384
|
+
title="Outcomes"
|
|
3385
|
+
/>
|
|
3386
|
+
</ChartSidebar>
|
|
3387
|
+
)
|
|
3388
|
+
}
|
|
3389
|
+
>
|
|
3390
|
+
{authSections.map((section) => (
|
|
3391
|
+
<AuthTraceSection
|
|
3392
|
+
authEvents={section.authEvents}
|
|
3393
|
+
key={section.id}
|
|
3394
|
+
label={section.label}
|
|
3395
|
+
onSelect={toggleEventSelection}
|
|
3396
|
+
selectedEventKey={selectedEventKey}
|
|
3397
|
+
trace={section.trace}
|
|
3398
|
+
/>
|
|
3399
|
+
))}
|
|
3400
|
+
</ProfilerSplitLayout>
|
|
3349
3401
|
);
|
|
3350
3402
|
};
|
|
3351
3403
|
|
|
@@ -3458,8 +3510,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3458
3510
|
});
|
|
3459
3511
|
|
|
3460
3512
|
return (
|
|
3461
|
-
|
|
3462
|
-
|
|
3513
|
+
<ProfilerSplitLayout
|
|
3514
|
+
sidebar={
|
|
3515
|
+
<ChartSidebar eyebrow="Summary charts" title="Session overview">
|
|
3463
3516
|
<ChartSection
|
|
3464
3517
|
emptyLabel="No recent session durations were captured yet."
|
|
3465
3518
|
options={durationTrendChart}
|
|
@@ -3484,8 +3537,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3484
3537
|
subtitle="Quick view of SSR, navigation, and request status patterns."
|
|
3485
3538
|
title="Status Spread"
|
|
3486
3539
|
/>
|
|
3487
|
-
|
|
3488
|
-
|
|
3540
|
+
</ChartSidebar>
|
|
3541
|
+
}
|
|
3542
|
+
>
|
|
3489
3543
|
<div className="proteum-profiler__metrics">
|
|
3490
3544
|
<SummaryRow label="Session" value={session.label} />
|
|
3491
3545
|
<SummaryRow label="Status" value={summary.statusLabel} />
|
|
@@ -3504,7 +3558,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3504
3558
|
<SummaryRow label="Errors" value={String(summary.errorCount)} />
|
|
3505
3559
|
<SummaryRow label="Request" value={session.requestId || 'client-only'} />
|
|
3506
3560
|
</div>
|
|
3507
|
-
|
|
3561
|
+
</ProfilerSplitLayout>
|
|
3508
3562
|
);
|
|
3509
3563
|
}
|
|
3510
3564
|
|
|
@@ -3552,7 +3606,42 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3552
3606
|
const memoryChart = perf.memory ? buildPerfMemoryChartOptions(perf.memory.rows) : undefined;
|
|
3553
3607
|
|
|
3554
3608
|
return (
|
|
3555
|
-
<
|
|
3609
|
+
<ProfilerSplitLayout
|
|
3610
|
+
sidebar={
|
|
3611
|
+
<ChartSidebar eyebrow="Performance charts" title="Performance overview">
|
|
3612
|
+
<WaterfallChart
|
|
3613
|
+
emptyLabel="No request stages were captured."
|
|
3614
|
+
itemLabel="stage"
|
|
3615
|
+
items={waterfallItems}
|
|
3616
|
+
/>
|
|
3617
|
+
<ChartSection
|
|
3618
|
+
emptyLabel="No hot-path latency data matched this window."
|
|
3619
|
+
options={topLatencyChart}
|
|
3620
|
+
subtitle={`Compare average and p95 latency across the hottest ${perf.groupBy}s.`}
|
|
3621
|
+
title={`Hot ${perf.groupBy}s`}
|
|
3622
|
+
/>
|
|
3623
|
+
<ChartSection
|
|
3624
|
+
emptyLabel="No breakdown data matched this window."
|
|
3625
|
+
options={topBreakdownChart}
|
|
3626
|
+
subtitle="See whether self time, SQL, external calls, or render work dominates the response."
|
|
3627
|
+
title="Time Breakdown"
|
|
3628
|
+
/>
|
|
3629
|
+
<ChartSection
|
|
3630
|
+
emptyLabel="No compare data matched these windows."
|
|
3631
|
+
options={compareChart}
|
|
3632
|
+
subtitle={`Track p95 regression pressure between ${perf.baseline} and ${perf.target}.`}
|
|
3633
|
+
title="Regression Delta"
|
|
3634
|
+
/>
|
|
3635
|
+
<ChartSection
|
|
3636
|
+
emptyLabel="No memory drift data matched this window."
|
|
3637
|
+
options={memoryChart}
|
|
3638
|
+
subtitle="Compare average heap growth, peak heap growth, and average RSS drift per group."
|
|
3639
|
+
title="Memory Drift"
|
|
3640
|
+
/>
|
|
3641
|
+
</ChartSidebar>
|
|
3642
|
+
}
|
|
3643
|
+
>
|
|
3644
|
+
<div className="proteum-profiler__section">
|
|
3556
3645
|
<div className="proteum-profiler__sectionHeader">
|
|
3557
3646
|
<div className="proteum-profiler__sectionTitle">Performance</div>
|
|
3558
3647
|
<div className="proteum-profiler__actions">
|
|
@@ -3646,11 +3735,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3646
3735
|
|
|
3647
3736
|
{currentRequest ? (
|
|
3648
3737
|
<>
|
|
3649
|
-
<WaterfallChart
|
|
3650
|
-
emptyLabel="No request stages were captured."
|
|
3651
|
-
itemLabel="stage"
|
|
3652
|
-
items={waterfallItems}
|
|
3653
|
-
/>
|
|
3654
3738
|
<div className="proteum-profiler__metrics">
|
|
3655
3739
|
<SummaryRow label="Route" value={currentRequest.routeLabel} />
|
|
3656
3740
|
<SummaryRow label="Controller" value={currentRequest.controllerLabel} />
|
|
@@ -3669,33 +3753,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3669
3753
|
<div className="proteum-profiler__empty">No traced request is attached to this session yet.</div>
|
|
3670
3754
|
)}
|
|
3671
3755
|
|
|
3672
|
-
<div className="proteum-profiler__chartGrid">
|
|
3673
|
-
<ChartSection
|
|
3674
|
-
emptyLabel="No hot-path latency data matched this window."
|
|
3675
|
-
options={topLatencyChart}
|
|
3676
|
-
subtitle={`Compare average and p95 latency across the hottest ${perf.groupBy}s.`}
|
|
3677
|
-
title={`Hot ${perf.groupBy}s`}
|
|
3678
|
-
/>
|
|
3679
|
-
<ChartSection
|
|
3680
|
-
emptyLabel="No breakdown data matched this window."
|
|
3681
|
-
options={topBreakdownChart}
|
|
3682
|
-
subtitle="See whether self time, SQL, external calls, or render work dominates the response."
|
|
3683
|
-
title="Time Breakdown"
|
|
3684
|
-
/>
|
|
3685
|
-
<ChartSection
|
|
3686
|
-
emptyLabel="No compare data matched these windows."
|
|
3687
|
-
options={compareChart}
|
|
3688
|
-
subtitle={`Track p95 regression pressure between ${perf.baseline} and ${perf.target}.`}
|
|
3689
|
-
title="Regression Delta"
|
|
3690
|
-
/>
|
|
3691
|
-
<ChartSection
|
|
3692
|
-
emptyLabel="No memory drift data matched this window."
|
|
3693
|
-
options={memoryChart}
|
|
3694
|
-
subtitle="Compare average heap growth, peak heap growth, and average RSS drift per group."
|
|
3695
|
-
title="Memory Drift"
|
|
3696
|
-
/>
|
|
3697
|
-
</div>
|
|
3698
|
-
|
|
3699
3756
|
<SimpleSection empty="No hot calls captured for this request." rows={callRows} title="Current Request Calls" />
|
|
3700
3757
|
<SimpleSection empty="No hot SQL captured for this request." rows={sqlRows} title="Current Request SQL" />
|
|
3701
3758
|
<SimpleSection empty="No perf rollups matched this window." rows={topRows} title={`Hot ${perf.groupBy}s`} />
|
|
@@ -3703,7 +3760,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3703
3760
|
<SimpleSection empty="No memory drift data matched this window." rows={memoryRows} title="Memory" />
|
|
3704
3761
|
</>
|
|
3705
3762
|
)}
|
|
3706
|
-
|
|
3763
|
+
</div>
|
|
3764
|
+
</ProfilerSplitLayout>
|
|
3707
3765
|
);
|
|
3708
3766
|
}
|
|
3709
3767
|
|
|
@@ -3761,8 +3819,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3761
3819
|
});
|
|
3762
3820
|
|
|
3763
3821
|
return (
|
|
3764
|
-
|
|
3765
|
-
|
|
3822
|
+
<ProfilerSplitLayout
|
|
3823
|
+
sidebar={
|
|
3824
|
+
<ChartSidebar eyebrow="Routing charts" title="Routing overview">
|
|
3766
3825
|
<ChartSection
|
|
3767
3826
|
emptyLabel="No routing events were captured yet."
|
|
3768
3827
|
options={routingFlowChart}
|
|
@@ -3781,8 +3840,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3781
3840
|
subtitle="Highlight skip reasons, matched routes, and controller routing outcomes."
|
|
3782
3841
|
title="Decisions"
|
|
3783
3842
|
/>
|
|
3784
|
-
|
|
3785
|
-
|
|
3843
|
+
</ChartSidebar>
|
|
3844
|
+
}
|
|
3845
|
+
>
|
|
3786
3846
|
<SimpleSection
|
|
3787
3847
|
empty="No routing data captured yet."
|
|
3788
3848
|
rows={routingEvents.map((event) => ({
|
|
@@ -3795,7 +3855,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3795
3855
|
showTitle={false}
|
|
3796
3856
|
title="Routing"
|
|
3797
3857
|
/>
|
|
3798
|
-
|
|
3858
|
+
</ProfilerSplitLayout>
|
|
3799
3859
|
);
|
|
3800
3860
|
}
|
|
3801
3861
|
|
|
@@ -3828,8 +3888,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3828
3888
|
});
|
|
3829
3889
|
|
|
3830
3890
|
return (
|
|
3831
|
-
|
|
3832
|
-
|
|
3891
|
+
<ProfilerSplitLayout
|
|
3892
|
+
sidebar={
|
|
3893
|
+
<ChartSidebar eyebrow="Controller charts" title="Controller overview">
|
|
3833
3894
|
<ChartSection
|
|
3834
3895
|
emptyLabel="No controller events were captured yet."
|
|
3835
3896
|
options={controllerFlowChart}
|
|
@@ -3848,8 +3909,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3848
3909
|
subtitle="Highlight which detail fields are most common across controller events."
|
|
3849
3910
|
title="Detail Keys"
|
|
3850
3911
|
/>
|
|
3851
|
-
|
|
3852
|
-
|
|
3912
|
+
</ChartSidebar>
|
|
3913
|
+
}
|
|
3914
|
+
>
|
|
3853
3915
|
<SimpleSection
|
|
3854
3916
|
empty="No controller data captured yet."
|
|
3855
3917
|
rows={controllerEvents.map((event) => ({
|
|
@@ -3862,7 +3924,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3862
3924
|
showTitle={false}
|
|
3863
3925
|
title="Controller"
|
|
3864
3926
|
/>
|
|
3865
|
-
|
|
3927
|
+
</ProfilerSplitLayout>
|
|
3866
3928
|
);
|
|
3867
3929
|
}
|
|
3868
3930
|
|
|
@@ -3906,8 +3968,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3906
3968
|
});
|
|
3907
3969
|
|
|
3908
3970
|
return (
|
|
3909
|
-
|
|
3910
|
-
|
|
3971
|
+
<ProfilerSplitLayout
|
|
3972
|
+
sidebar={
|
|
3973
|
+
<ChartSidebar eyebrow="SSR charts" title="SSR overview">
|
|
3911
3974
|
<ChartSection
|
|
3912
3975
|
emptyLabel="No SSR timing and payload data was captured yet."
|
|
3913
3976
|
options={ssrScatterChart}
|
|
@@ -3926,8 +3989,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3926
3989
|
subtitle="Track render time across the last few SSR sessions."
|
|
3927
3990
|
title="Render Trend"
|
|
3928
3991
|
/>
|
|
3929
|
-
|
|
3930
|
-
|
|
3992
|
+
</ChartSidebar>
|
|
3993
|
+
}
|
|
3994
|
+
>
|
|
3931
3995
|
<SimpleSection
|
|
3932
3996
|
empty="No SSR data captured for this session."
|
|
3933
3997
|
rows={ssrEvents.map((event) => ({
|
|
@@ -3940,7 +4004,7 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
3940
4004
|
showTitle={false}
|
|
3941
4005
|
title="SSR"
|
|
3942
4006
|
/>
|
|
3943
|
-
|
|
4007
|
+
</ProfilerSplitLayout>
|
|
3944
4008
|
);
|
|
3945
4009
|
}
|
|
3946
4010
|
|
|
@@ -4024,7 +4088,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4024
4088
|
})) || [];
|
|
4025
4089
|
|
|
4026
4090
|
return (
|
|
4027
|
-
<
|
|
4091
|
+
<ProfilerSplitLayout
|
|
4092
|
+
sidebar={
|
|
4093
|
+
<ChartSidebar eyebrow="Diagnose charts" title="Diagnose overview">
|
|
4094
|
+
<ChartSection
|
|
4095
|
+
emptyLabel="No suspect scores were returned for this diagnose run."
|
|
4096
|
+
options={suspectScoreChart}
|
|
4097
|
+
subtitle="Rank the files Proteum currently believes are most likely involved."
|
|
4098
|
+
title="Suspects"
|
|
4099
|
+
/>
|
|
4100
|
+
<ChartSection
|
|
4101
|
+
emptyLabel="No owner matches were returned for this diagnose run."
|
|
4102
|
+
options={ownerScoreChart}
|
|
4103
|
+
subtitle="Compare owner candidates and their confidence scores."
|
|
4104
|
+
title="Owner Matches"
|
|
4105
|
+
/>
|
|
4106
|
+
<ChartSection
|
|
4107
|
+
emptyLabel="No diagnose severity data was returned for this run."
|
|
4108
|
+
options={diagnoseSeverityChart}
|
|
4109
|
+
subtitle="Compare doctor diagnostics against contract diagnostics in one view."
|
|
4110
|
+
title="Severity"
|
|
4111
|
+
/>
|
|
4112
|
+
</ChartSidebar>
|
|
4113
|
+
}
|
|
4114
|
+
>
|
|
4115
|
+
<div className="proteum-profiler__section">
|
|
4028
4116
|
<div className="proteum-profiler__sectionHeader">
|
|
4029
4117
|
<div className="proteum-profiler__sectionTitle">Diagnose</div>
|
|
4030
4118
|
<div className="proteum-profiler__actions">
|
|
@@ -4059,33 +4147,14 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4059
4147
|
value={diagnose.lastLoadedAt ? formatTimestamp(diagnose.lastLoadedAt) : 'Not loaded'}
|
|
4060
4148
|
/>
|
|
4061
4149
|
</div>
|
|
4062
|
-
<div className="proteum-profiler__chartGrid">
|
|
4063
|
-
<ChartSection
|
|
4064
|
-
emptyLabel="No suspect scores were returned for this diagnose run."
|
|
4065
|
-
options={suspectScoreChart}
|
|
4066
|
-
subtitle="Rank the files Proteum currently believes are most likely involved."
|
|
4067
|
-
title="Suspects"
|
|
4068
|
-
/>
|
|
4069
|
-
<ChartSection
|
|
4070
|
-
emptyLabel="No owner matches were returned for this diagnose run."
|
|
4071
|
-
options={ownerScoreChart}
|
|
4072
|
-
subtitle="Compare owner candidates and their confidence scores."
|
|
4073
|
-
title="Owner Matches"
|
|
4074
|
-
/>
|
|
4075
|
-
<ChartSection
|
|
4076
|
-
emptyLabel="No diagnose severity data was returned for this run."
|
|
4077
|
-
options={diagnoseSeverityChart}
|
|
4078
|
-
subtitle="Compare doctor diagnostics against contract diagnostics in one view."
|
|
4079
|
-
title="Severity"
|
|
4080
|
-
/>
|
|
4081
|
-
</div>
|
|
4082
4150
|
<SimpleSection empty="No likely suspect files were found." rows={suspectRows} title="Suspects" />
|
|
4083
4151
|
<SimpleSection empty="No owner matches were found." rows={ownerRows} title="Owner Matches" />
|
|
4084
4152
|
<SimpleSection empty="No contract diagnostics were found." rows={contractRows} title="Contracts" />
|
|
4085
4153
|
<SimpleSection empty="No recent server logs were captured." rows={logRows} title="Server Logs" />
|
|
4086
4154
|
</>
|
|
4087
4155
|
)}
|
|
4088
|
-
|
|
4156
|
+
</div>
|
|
4157
|
+
</ProfilerSplitLayout>
|
|
4089
4158
|
);
|
|
4090
4159
|
}
|
|
4091
4160
|
|
|
@@ -4167,7 +4236,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4167
4236
|
: undefined;
|
|
4168
4237
|
|
|
4169
4238
|
return (
|
|
4170
|
-
<
|
|
4239
|
+
<ProfilerSplitLayout
|
|
4240
|
+
sidebar={
|
|
4241
|
+
<ChartSidebar eyebrow="Explain charts" title="Manifest overview">
|
|
4242
|
+
<ChartSection
|
|
4243
|
+
emptyLabel="No manifest structure data is available."
|
|
4244
|
+
options={structureChart}
|
|
4245
|
+
subtitle="Summarize the main object counts in the current Proteum manifest."
|
|
4246
|
+
title="Structure"
|
|
4247
|
+
/>
|
|
4248
|
+
<ChartSection
|
|
4249
|
+
emptyLabel="No manifest scope data is available."
|
|
4250
|
+
options={manifestScopeChart}
|
|
4251
|
+
subtitle="Show how much of the current manifest comes from app code vs framework code."
|
|
4252
|
+
title="Scope Split"
|
|
4253
|
+
/>
|
|
4254
|
+
<ChartSection
|
|
4255
|
+
emptyLabel="No manifest env readiness data is available."
|
|
4256
|
+
options={envReadinessChart}
|
|
4257
|
+
subtitle="Check required variable coverage before digging through the raw manifest."
|
|
4258
|
+
title="Env Ready"
|
|
4259
|
+
/>
|
|
4260
|
+
</ChartSidebar>
|
|
4261
|
+
}
|
|
4262
|
+
>
|
|
4263
|
+
<div className="proteum-profiler__section">
|
|
4171
4264
|
<div className="proteum-profiler__sectionHeader">
|
|
4172
4265
|
<div className="proteum-profiler__sectionTitle">Explain</div>
|
|
4173
4266
|
<div className="proteum-profiler__actions">
|
|
@@ -4192,27 +4285,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4192
4285
|
<div className="proteum-profiler__empty">No explain manifest is available.</div>
|
|
4193
4286
|
) : (
|
|
4194
4287
|
<>
|
|
4195
|
-
<div className="proteum-profiler__chartGrid">
|
|
4196
|
-
<ChartSection
|
|
4197
|
-
emptyLabel="No manifest structure data is available."
|
|
4198
|
-
options={structureChart}
|
|
4199
|
-
subtitle="Summarize the main object counts in the current Proteum manifest."
|
|
4200
|
-
title="Structure"
|
|
4201
|
-
/>
|
|
4202
|
-
<ChartSection
|
|
4203
|
-
emptyLabel="No manifest scope data is available."
|
|
4204
|
-
options={manifestScopeChart}
|
|
4205
|
-
subtitle="Show how much of the current manifest comes from app code vs framework code."
|
|
4206
|
-
title="Scope Split"
|
|
4207
|
-
/>
|
|
4208
|
-
<ChartSection
|
|
4209
|
-
emptyLabel="No manifest env readiness data is available."
|
|
4210
|
-
options={envReadinessChart}
|
|
4211
|
-
subtitle="Check required variable coverage before digging through the raw manifest."
|
|
4212
|
-
title="Env Ready"
|
|
4213
|
-
/>
|
|
4214
|
-
</div>
|
|
4215
|
-
|
|
4216
4288
|
<div className="proteum-profiler__row">
|
|
4217
4289
|
<div className="proteum-profiler__rowHeader">
|
|
4218
4290
|
<strong>Manifest snapshot</strong>
|
|
@@ -4227,7 +4299,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4227
4299
|
<TextBlocks blocks={blocks} />
|
|
4228
4300
|
</>
|
|
4229
4301
|
)}
|
|
4230
|
-
|
|
4302
|
+
</div>
|
|
4303
|
+
</ProfilerSplitLayout>
|
|
4231
4304
|
);
|
|
4232
4305
|
}
|
|
4233
4306
|
|
|
@@ -4289,7 +4362,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4289
4362
|
const doctorBlocks = state.explain.manifest ? buildDoctorBlocks(state.explain.manifest) : [];
|
|
4290
4363
|
|
|
4291
4364
|
return (
|
|
4292
|
-
<
|
|
4365
|
+
<ProfilerSplitLayout
|
|
4366
|
+
sidebar={
|
|
4367
|
+
<ChartSidebar eyebrow="Doctor charts" title="Doctor overview">
|
|
4368
|
+
<ChartSection
|
|
4369
|
+
emptyLabel="No doctor severity data is available."
|
|
4370
|
+
options={doctorSeverityChart}
|
|
4371
|
+
subtitle="Compare doctor and contract diagnostics across errors and warnings."
|
|
4372
|
+
title="Severity"
|
|
4373
|
+
/>
|
|
4374
|
+
<ChartSection
|
|
4375
|
+
emptyLabel="No diagnostic codes are available."
|
|
4376
|
+
options={doctorCodeChart}
|
|
4377
|
+
subtitle="See which diagnostic families are dominating the current manifest."
|
|
4378
|
+
title="Codes"
|
|
4379
|
+
/>
|
|
4380
|
+
<ChartSection
|
|
4381
|
+
emptyLabel="No diagnostic file hotspots are available."
|
|
4382
|
+
options={doctorFileChart}
|
|
4383
|
+
subtitle="Highlight the files attracting the most diagnostics."
|
|
4384
|
+
title="Files"
|
|
4385
|
+
/>
|
|
4386
|
+
</ChartSidebar>
|
|
4387
|
+
}
|
|
4388
|
+
>
|
|
4389
|
+
<div className="proteum-profiler__section">
|
|
4293
4390
|
<div className="proteum-profiler__sectionHeader">
|
|
4294
4391
|
<div className="proteum-profiler__sectionTitle">Doctor</div>
|
|
4295
4392
|
<div className="proteum-profiler__actions">
|
|
@@ -4331,26 +4428,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4331
4428
|
value={doctor.lastLoadedAt ? formatTimestamp(doctor.lastLoadedAt) : 'Not loaded'}
|
|
4332
4429
|
/>
|
|
4333
4430
|
</div>
|
|
4334
|
-
<div className="proteum-profiler__chartGrid">
|
|
4335
|
-
<ChartSection
|
|
4336
|
-
emptyLabel="No doctor severity data is available."
|
|
4337
|
-
options={doctorSeverityChart}
|
|
4338
|
-
subtitle="Compare doctor and contract diagnostics across errors and warnings."
|
|
4339
|
-
title="Severity"
|
|
4340
|
-
/>
|
|
4341
|
-
<ChartSection
|
|
4342
|
-
emptyLabel="No diagnostic codes are available."
|
|
4343
|
-
options={doctorCodeChart}
|
|
4344
|
-
subtitle="See which diagnostic families are dominating the current manifest."
|
|
4345
|
-
title="Codes"
|
|
4346
|
-
/>
|
|
4347
|
-
<ChartSection
|
|
4348
|
-
emptyLabel="No diagnostic file hotspots are available."
|
|
4349
|
-
options={doctorFileChart}
|
|
4350
|
-
subtitle="Highlight the files attracting the most diagnostics."
|
|
4351
|
-
title="Files"
|
|
4352
|
-
/>
|
|
4353
|
-
</div>
|
|
4354
4431
|
{doctorBlocks.length > 0 ? (
|
|
4355
4432
|
<TextBlocks blocks={doctorBlocks} />
|
|
4356
4433
|
) : (
|
|
@@ -4359,7 +4436,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4359
4436
|
<SimpleSection empty="No contract diagnostics were found." rows={contractRows} title="Contracts" />
|
|
4360
4437
|
</>
|
|
4361
4438
|
)}
|
|
4362
|
-
|
|
4439
|
+
</div>
|
|
4440
|
+
</ProfilerSplitLayout>
|
|
4363
4441
|
);
|
|
4364
4442
|
}
|
|
4365
4443
|
|
|
@@ -4394,7 +4472,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4394
4472
|
});
|
|
4395
4473
|
|
|
4396
4474
|
return (
|
|
4397
|
-
<
|
|
4475
|
+
<ProfilerSplitLayout
|
|
4476
|
+
sidebar={
|
|
4477
|
+
<ChartSidebar eyebrow="Command charts" title="Command overview">
|
|
4478
|
+
<ChartSection
|
|
4479
|
+
emptyLabel="No command scope data is available."
|
|
4480
|
+
options={commandScopeChart}
|
|
4481
|
+
subtitle="Show how the registered dev commands split between app and framework scopes."
|
|
4482
|
+
title="Scope"
|
|
4483
|
+
/>
|
|
4484
|
+
<ChartSection
|
|
4485
|
+
emptyLabel="No command execution durations have been captured yet."
|
|
4486
|
+
options={commandDurationChart}
|
|
4487
|
+
subtitle="Highlight the commands with the slowest latest execution."
|
|
4488
|
+
title="Latest Duration"
|
|
4489
|
+
/>
|
|
4490
|
+
<ChartSection
|
|
4491
|
+
emptyLabel="No command execution statuses have been captured yet."
|
|
4492
|
+
options={commandStatusChart}
|
|
4493
|
+
subtitle="Separate commands that have never run from completed or failed commands."
|
|
4494
|
+
title="Status"
|
|
4495
|
+
/>
|
|
4496
|
+
</ChartSidebar>
|
|
4497
|
+
}
|
|
4498
|
+
>
|
|
4499
|
+
<div className="proteum-profiler__section">
|
|
4398
4500
|
<div className="proteum-profiler__sectionHeader">
|
|
4399
4501
|
<div className="proteum-profiler__sectionTitle">Available commands</div>
|
|
4400
4502
|
<div className="proteum-profiler__actions">
|
|
@@ -4431,27 +4533,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4431
4533
|
<div className="proteum-profiler__empty">No commands are registered for this app.</div>
|
|
4432
4534
|
) : (
|
|
4433
4535
|
<>
|
|
4434
|
-
<div className="proteum-profiler__chartGrid">
|
|
4435
|
-
<ChartSection
|
|
4436
|
-
emptyLabel="No command scope data is available."
|
|
4437
|
-
options={commandScopeChart}
|
|
4438
|
-
subtitle="Show how the registered dev commands split between app and framework scopes."
|
|
4439
|
-
title="Scope"
|
|
4440
|
-
/>
|
|
4441
|
-
<ChartSection
|
|
4442
|
-
emptyLabel="No command execution durations have been captured yet."
|
|
4443
|
-
options={commandDurationChart}
|
|
4444
|
-
subtitle="Highlight the commands with the slowest latest execution."
|
|
4445
|
-
title="Latest Duration"
|
|
4446
|
-
/>
|
|
4447
|
-
<ChartSection
|
|
4448
|
-
emptyLabel="No command execution statuses have been captured yet."
|
|
4449
|
-
options={commandStatusChart}
|
|
4450
|
-
subtitle="Separate commands that have never run from completed or failed commands."
|
|
4451
|
-
title="Status"
|
|
4452
|
-
/>
|
|
4453
|
-
</div>
|
|
4454
|
-
|
|
4455
4536
|
<div className="proteum-profiler__list">
|
|
4456
4537
|
{commandsState.commands.map((command: TDevCommandDefinition) => {
|
|
4457
4538
|
const execution = commandsState.executions[command.path] as TDevCommandExecution | undefined;
|
|
@@ -4513,7 +4594,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4513
4594
|
</div>
|
|
4514
4595
|
</>
|
|
4515
4596
|
)}
|
|
4516
|
-
|
|
4597
|
+
</div>
|
|
4598
|
+
</ProfilerSplitLayout>
|
|
4517
4599
|
);
|
|
4518
4600
|
}
|
|
4519
4601
|
|
|
@@ -4545,7 +4627,31 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4545
4627
|
});
|
|
4546
4628
|
|
|
4547
4629
|
return (
|
|
4548
|
-
<
|
|
4630
|
+
<ProfilerSplitLayout
|
|
4631
|
+
sidebar={
|
|
4632
|
+
<ChartSidebar eyebrow="Cron charts" title="Cron overview">
|
|
4633
|
+
<ChartSection
|
|
4634
|
+
emptyLabel="No cron run counts are available."
|
|
4635
|
+
options={cronRunCountChart}
|
|
4636
|
+
subtitle="Highlight which tasks have been exercised the most in the current dev session."
|
|
4637
|
+
title="Runs"
|
|
4638
|
+
/>
|
|
4639
|
+
<ChartSection
|
|
4640
|
+
emptyLabel="No cron duration data is available."
|
|
4641
|
+
options={cronDurationChart}
|
|
4642
|
+
subtitle="Compare the latest duration of tasks that have been executed."
|
|
4643
|
+
title="Duration"
|
|
4644
|
+
/>
|
|
4645
|
+
<ChartSection
|
|
4646
|
+
emptyLabel="No cron status data is available."
|
|
4647
|
+
options={cronStatusChart}
|
|
4648
|
+
subtitle="Separate currently running, completed, failed, and never-run tasks."
|
|
4649
|
+
title="Status"
|
|
4650
|
+
/>
|
|
4651
|
+
</ChartSidebar>
|
|
4652
|
+
}
|
|
4653
|
+
>
|
|
4654
|
+
<div className="proteum-profiler__section">
|
|
4549
4655
|
<div className="proteum-profiler__sectionHeader">
|
|
4550
4656
|
<div className="proteum-profiler__sectionTitle">Registered tasks</div>
|
|
4551
4657
|
<div className="proteum-profiler__actions">
|
|
@@ -4583,27 +4689,6 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4583
4689
|
<div className="proteum-profiler__empty">No cron tasks are registered for this app.</div>
|
|
4584
4690
|
) : (
|
|
4585
4691
|
<>
|
|
4586
|
-
<div className="proteum-profiler__chartGrid">
|
|
4587
|
-
<ChartSection
|
|
4588
|
-
emptyLabel="No cron run counts are available."
|
|
4589
|
-
options={cronRunCountChart}
|
|
4590
|
-
subtitle="Highlight which tasks have been exercised the most in the current dev session."
|
|
4591
|
-
title="Runs"
|
|
4592
|
-
/>
|
|
4593
|
-
<ChartSection
|
|
4594
|
-
emptyLabel="No cron duration data is available."
|
|
4595
|
-
options={cronDurationChart}
|
|
4596
|
-
subtitle="Compare the latest duration of tasks that have been executed."
|
|
4597
|
-
title="Duration"
|
|
4598
|
-
/>
|
|
4599
|
-
<ChartSection
|
|
4600
|
-
emptyLabel="No cron status data is available."
|
|
4601
|
-
options={cronStatusChart}
|
|
4602
|
-
subtitle="Separate currently running, completed, failed, and never-run tasks."
|
|
4603
|
-
title="Status"
|
|
4604
|
-
/>
|
|
4605
|
-
</div>
|
|
4606
|
-
|
|
4607
4692
|
<div className="proteum-profiler__list">
|
|
4608
4693
|
{cron.tasks.map((task) => (
|
|
4609
4694
|
<div className="proteum-profiler__row" key={task.name}>
|
|
@@ -4657,7 +4742,8 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4657
4742
|
</div>
|
|
4658
4743
|
</>
|
|
4659
4744
|
)}
|
|
4660
|
-
|
|
4745
|
+
</div>
|
|
4746
|
+
</ProfilerSplitLayout>
|
|
4661
4747
|
);
|
|
4662
4748
|
}
|
|
4663
4749
|
|
|
@@ -4693,8 +4779,9 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4693
4779
|
});
|
|
4694
4780
|
|
|
4695
4781
|
return (
|
|
4696
|
-
|
|
4697
|
-
|
|
4782
|
+
<ProfilerSplitLayout
|
|
4783
|
+
sidebar={
|
|
4784
|
+
<ChartSidebar eyebrow="Error charts" title="Error overview">
|
|
4698
4785
|
<ChartSection
|
|
4699
4786
|
emptyLabel="No errors were captured for this session."
|
|
4700
4787
|
options={errorSourceChart}
|
|
@@ -4707,17 +4794,145 @@ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession,
|
|
|
4707
4794
|
subtitle="Highlight the error families that are repeating in the selected session."
|
|
4708
4795
|
title="Groups"
|
|
4709
4796
|
/>
|
|
4710
|
-
|
|
4711
|
-
|
|
4797
|
+
</ChartSidebar>
|
|
4798
|
+
}
|
|
4799
|
+
>
|
|
4712
4800
|
<SimpleSection empty="No errors captured." rows={errorRows} showTitle={false} title="Errors" />
|
|
4713
|
-
|
|
4801
|
+
</ProfilerSplitLayout>
|
|
4714
4802
|
);
|
|
4715
4803
|
};
|
|
4716
4804
|
|
|
4717
|
-
const splitScrollPanels = new Set<TProfilerPanel>(
|
|
4805
|
+
const splitScrollPanels = new Set<TProfilerPanel>(Object.keys(panelLabels) as TProfilerPanel[]);
|
|
4806
|
+
const profilerHandleStorageKey = 'proteum.dev.profiler.handle-position';
|
|
4807
|
+
const profilerHandleSize = 46;
|
|
4808
|
+
const profilerHandleViewportMargin = 10;
|
|
4809
|
+
|
|
4810
|
+
type TProfilerHandlePosition = {
|
|
4811
|
+
x: number;
|
|
4812
|
+
y: number;
|
|
4813
|
+
};
|
|
4814
|
+
|
|
4815
|
+
type TProfilerHandleDragState = {
|
|
4816
|
+
element: HTMLButtonElement;
|
|
4817
|
+
moved: boolean;
|
|
4818
|
+
origin: TProfilerHandlePosition;
|
|
4819
|
+
pointerId: number;
|
|
4820
|
+
size: number;
|
|
4821
|
+
startX: number;
|
|
4822
|
+
startY: number;
|
|
4823
|
+
};
|
|
4824
|
+
|
|
4825
|
+
const clampProfilerHandlePosition = (position: TProfilerHandlePosition, size = profilerHandleSize): TProfilerHandlePosition => {
|
|
4826
|
+
if (typeof window === 'undefined') return position;
|
|
4827
|
+
|
|
4828
|
+
const maxX = Math.max(profilerHandleViewportMargin, window.innerWidth - size - profilerHandleViewportMargin);
|
|
4829
|
+
const maxY = Math.max(profilerHandleViewportMargin, window.innerHeight - size - profilerHandleViewportMargin);
|
|
4830
|
+
|
|
4831
|
+
return {
|
|
4832
|
+
x: Math.min(Math.max(position.x, profilerHandleViewportMargin), maxX),
|
|
4833
|
+
y: Math.min(Math.max(position.y, profilerHandleViewportMargin), maxY),
|
|
4834
|
+
};
|
|
4835
|
+
};
|
|
4836
|
+
|
|
4837
|
+
const isProfilerHandlePosition = (value: unknown): value is TProfilerHandlePosition => {
|
|
4838
|
+
if (!value || typeof value !== 'object') return false;
|
|
4839
|
+
|
|
4840
|
+
const position = value as Partial<TProfilerHandlePosition>;
|
|
4841
|
+
return typeof position.x === 'number' && Number.isFinite(position.x) && typeof position.y === 'number' && Number.isFinite(position.y);
|
|
4842
|
+
};
|
|
4843
|
+
|
|
4844
|
+
const readProfilerHandlePosition = (): TProfilerHandlePosition | undefined => {
|
|
4845
|
+
if (typeof window === 'undefined') return undefined;
|
|
4846
|
+
|
|
4847
|
+
try {
|
|
4848
|
+
const value = window.sessionStorage.getItem(profilerHandleStorageKey);
|
|
4849
|
+
if (!value) return undefined;
|
|
4850
|
+
|
|
4851
|
+
const position = JSON.parse(value) as unknown;
|
|
4852
|
+
return isProfilerHandlePosition(position) ? clampProfilerHandlePosition(position) : undefined;
|
|
4853
|
+
} catch (error) {
|
|
4854
|
+
return undefined;
|
|
4855
|
+
}
|
|
4856
|
+
};
|
|
4857
|
+
|
|
4858
|
+
const writeProfilerHandlePosition = (position: TProfilerHandlePosition) => {
|
|
4859
|
+
if (typeof window === 'undefined') return;
|
|
4860
|
+
|
|
4861
|
+
try {
|
|
4862
|
+
window.sessionStorage.setItem(profilerHandleStorageKey, JSON.stringify(position));
|
|
4863
|
+
} catch (error) {}
|
|
4864
|
+
};
|
|
4865
|
+
|
|
4866
|
+
const getProfilerHandleElementPosition = (element: HTMLElement): TProfilerHandlePosition => {
|
|
4867
|
+
const rect = element.getBoundingClientRect();
|
|
4868
|
+
return { x: rect.left, y: rect.top };
|
|
4869
|
+
};
|
|
4870
|
+
|
|
4871
|
+
type TProfilerHandleMoveEvent = {
|
|
4872
|
+
clientX: number;
|
|
4873
|
+
clientY: number;
|
|
4874
|
+
pointerId?: number;
|
|
4875
|
+
preventDefault?: () => void;
|
|
4876
|
+
};
|
|
4877
|
+
|
|
4878
|
+
type TProfilerHandleEndEvent = {
|
|
4879
|
+
pointerId?: number;
|
|
4880
|
+
};
|
|
4881
|
+
|
|
4882
|
+
const AtomIcon = () => (
|
|
4883
|
+
<svg aria-hidden="true" className="proteum-profiler__handleIcon" fill="none" viewBox="0 0 24 24">
|
|
4884
|
+
<circle cx="12" cy="12" fill="currentColor" r="1.9" />
|
|
4885
|
+
<ellipse cx="12" cy="12" rx="9" ry="3.7" stroke="currentColor" strokeWidth="1.7" />
|
|
4886
|
+
<ellipse cx="12" cy="12" rx="9" ry="3.7" stroke="currentColor" strokeWidth="1.7" transform="rotate(60 12 12)" />
|
|
4887
|
+
<ellipse cx="12" cy="12" rx="9" ry="3.7" stroke="currentColor" strokeWidth="1.7" transform="rotate(120 12 12)" />
|
|
4888
|
+
</svg>
|
|
4889
|
+
);
|
|
4718
4890
|
|
|
4719
4891
|
export default function DevProfiler() {
|
|
4720
4892
|
const [state, setState] = React.useState(() => profilerRuntime.getState());
|
|
4893
|
+
const [handlePosition, setHandlePosition] = React.useState<TProfilerHandlePosition | undefined>(() => readProfilerHandlePosition());
|
|
4894
|
+
const [isHandleDragging, setIsHandleDragging] = React.useState(false);
|
|
4895
|
+
const handleDragStateRef = React.useRef<TProfilerHandleDragState | undefined>(undefined);
|
|
4896
|
+
const ignoreHandleClickRef = React.useRef(false);
|
|
4897
|
+
|
|
4898
|
+
const dragHandle = React.useCallback((event: TProfilerHandleMoveEvent) => {
|
|
4899
|
+
const dragState = handleDragStateRef.current;
|
|
4900
|
+
if (!dragState || (event.pointerId !== undefined && dragState.pointerId !== event.pointerId)) return;
|
|
4901
|
+
|
|
4902
|
+
const deltaX = event.clientX - dragState.startX;
|
|
4903
|
+
const deltaY = event.clientY - dragState.startY;
|
|
4904
|
+
if (!dragState.moved && Math.hypot(deltaX, deltaY) > 3) dragState.moved = true;
|
|
4905
|
+
|
|
4906
|
+
const nextPosition = clampProfilerHandlePosition(
|
|
4907
|
+
{
|
|
4908
|
+
x: dragState.origin.x + deltaX,
|
|
4909
|
+
y: dragState.origin.y + deltaY,
|
|
4910
|
+
},
|
|
4911
|
+
dragState.size,
|
|
4912
|
+
);
|
|
4913
|
+
|
|
4914
|
+
setHandlePosition(nextPosition);
|
|
4915
|
+
writeProfilerHandlePosition(nextPosition);
|
|
4916
|
+
event.preventDefault?.();
|
|
4917
|
+
}, []);
|
|
4918
|
+
|
|
4919
|
+
const finishHandleDrag = React.useCallback((event: TProfilerHandleEndEvent) => {
|
|
4920
|
+
const dragState = handleDragStateRef.current;
|
|
4921
|
+
if (!dragState || (event.pointerId !== undefined && dragState.pointerId !== event.pointerId)) return;
|
|
4922
|
+
|
|
4923
|
+
if (dragState.moved) {
|
|
4924
|
+
ignoreHandleClickRef.current = true;
|
|
4925
|
+
window.setTimeout(() => {
|
|
4926
|
+
ignoreHandleClickRef.current = false;
|
|
4927
|
+
}, 250);
|
|
4928
|
+
}
|
|
4929
|
+
|
|
4930
|
+
handleDragStateRef.current = undefined;
|
|
4931
|
+
setIsHandleDragging(false);
|
|
4932
|
+
if (dragState.element.hasPointerCapture(dragState.pointerId)) {
|
|
4933
|
+
dragState.element.releasePointerCapture(dragState.pointerId);
|
|
4934
|
+
}
|
|
4935
|
+
}, []);
|
|
4721
4936
|
|
|
4722
4937
|
React.useEffect(() => profilerRuntime.subscribe(() => setState(profilerRuntime.getState())), []);
|
|
4723
4938
|
React.useEffect(() => {
|
|
@@ -4736,6 +4951,81 @@ export default function DevProfiler() {
|
|
|
4736
4951
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
4737
4952
|
}, []);
|
|
4738
4953
|
|
|
4954
|
+
React.useEffect(() => {
|
|
4955
|
+
const onResize = () => {
|
|
4956
|
+
setHandlePosition((position: TProfilerHandlePosition | undefined) => {
|
|
4957
|
+
if (!position) return position;
|
|
4958
|
+
|
|
4959
|
+
const nextPosition = clampProfilerHandlePosition(position);
|
|
4960
|
+
writeProfilerHandlePosition(nextPosition);
|
|
4961
|
+
return nextPosition;
|
|
4962
|
+
});
|
|
4963
|
+
};
|
|
4964
|
+
|
|
4965
|
+
window.addEventListener('resize', onResize);
|
|
4966
|
+
return () => window.removeEventListener('resize', onResize);
|
|
4967
|
+
}, []);
|
|
4968
|
+
|
|
4969
|
+
React.useEffect(() => {
|
|
4970
|
+
if (!isHandleDragging) return;
|
|
4971
|
+
|
|
4972
|
+
const onPointerMove = (event: PointerEvent) => dragHandle(event);
|
|
4973
|
+
const onPointerEnd = (event: PointerEvent) => finishHandleDrag(event);
|
|
4974
|
+
const onMouseMove = (event: MouseEvent) => dragHandle(event);
|
|
4975
|
+
const onMouseEnd = () => finishHandleDrag({});
|
|
4976
|
+
const eventOptions: AddEventListenerOptions = { passive: false };
|
|
4977
|
+
|
|
4978
|
+
window.addEventListener('pointermove', onPointerMove, eventOptions);
|
|
4979
|
+
window.addEventListener('pointerup', onPointerEnd);
|
|
4980
|
+
window.addEventListener('pointercancel', onPointerEnd);
|
|
4981
|
+
window.addEventListener('mousemove', onMouseMove, eventOptions);
|
|
4982
|
+
window.addEventListener('mouseup', onMouseEnd);
|
|
4983
|
+
|
|
4984
|
+
return () => {
|
|
4985
|
+
window.removeEventListener('pointermove', onPointerMove, eventOptions);
|
|
4986
|
+
window.removeEventListener('pointerup', onPointerEnd);
|
|
4987
|
+
window.removeEventListener('pointercancel', onPointerEnd);
|
|
4988
|
+
window.removeEventListener('mousemove', onMouseMove, eventOptions);
|
|
4989
|
+
window.removeEventListener('mouseup', onMouseEnd);
|
|
4990
|
+
};
|
|
4991
|
+
}, [dragHandle, finishHandleDrag, isHandleDragging]);
|
|
4992
|
+
|
|
4993
|
+
const beginHandleDrag = React.useCallback(
|
|
4994
|
+
(event: React.PointerEvent<HTMLButtonElement>) => {
|
|
4995
|
+
if (event.button !== 0) return;
|
|
4996
|
+
|
|
4997
|
+
const element = event.currentTarget;
|
|
4998
|
+
const rect = element.getBoundingClientRect();
|
|
4999
|
+
const origin = handlePosition || getProfilerHandleElementPosition(element);
|
|
5000
|
+
|
|
5001
|
+
handleDragStateRef.current = {
|
|
5002
|
+
element,
|
|
5003
|
+
moved: false,
|
|
5004
|
+
origin,
|
|
5005
|
+
pointerId: event.pointerId,
|
|
5006
|
+
size: Math.max(rect.width, rect.height, profilerHandleSize),
|
|
5007
|
+
startX: event.clientX,
|
|
5008
|
+
startY: event.clientY,
|
|
5009
|
+
};
|
|
5010
|
+
|
|
5011
|
+
setIsHandleDragging(true);
|
|
5012
|
+
element.setPointerCapture(event.pointerId);
|
|
5013
|
+
event.preventDefault();
|
|
5014
|
+
},
|
|
5015
|
+
[handlePosition],
|
|
5016
|
+
);
|
|
5017
|
+
|
|
5018
|
+
const openProfilerFromHandle = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
|
5019
|
+
if (ignoreHandleClickRef.current) {
|
|
5020
|
+
ignoreHandleClickRef.current = false;
|
|
5021
|
+
event.preventDefault();
|
|
5022
|
+
event.stopPropagation();
|
|
5023
|
+
return;
|
|
5024
|
+
}
|
|
5025
|
+
|
|
5026
|
+
profilerRuntime.setUiState('minimized');
|
|
5027
|
+
}, []);
|
|
5028
|
+
|
|
4739
5029
|
if (!window.dev) return null;
|
|
4740
5030
|
|
|
4741
5031
|
const session = getSelectedSession(state.sessions, state.selectedSessionId, state.currentSessionId);
|
|
@@ -4763,11 +5053,27 @@ export default function DevProfiler() {
|
|
|
4763
5053
|
|
|
4764
5054
|
{state.uiState === 'pinned-handle' ? (
|
|
4765
5055
|
<button
|
|
4766
|
-
|
|
4767
|
-
|
|
5056
|
+
aria-label="Open Proteum profiler"
|
|
5057
|
+
className={isHandleDragging ? 'proteum-profiler__handle proteum-profiler__handle--dragging' : 'proteum-profiler__handle'}
|
|
5058
|
+
onClick={openProfilerFromHandle}
|
|
5059
|
+
onPointerCancel={finishHandleDrag}
|
|
5060
|
+
onPointerDown={beginHandleDrag}
|
|
5061
|
+
onPointerMove={dragHandle}
|
|
5062
|
+
onPointerUp={finishHandleDrag}
|
|
5063
|
+
style={
|
|
5064
|
+
handlePosition
|
|
5065
|
+
? {
|
|
5066
|
+
bottom: 'auto',
|
|
5067
|
+
left: handlePosition.x,
|
|
5068
|
+
right: 'auto',
|
|
5069
|
+
top: handlePosition.y,
|
|
5070
|
+
}
|
|
5071
|
+
: undefined
|
|
5072
|
+
}
|
|
5073
|
+
title="Open Proteum profiler"
|
|
4768
5074
|
type="button"
|
|
4769
5075
|
>
|
|
4770
|
-
|
|
5076
|
+
<AtomIcon />
|
|
4771
5077
|
</button>
|
|
4772
5078
|
) : (
|
|
4773
5079
|
<>
|