Skip to content

Commit 84dbaa5

Browse files
authored
feat(laravel-insights): Add commands and jobs table (#92924)
Add jobs and commands table to the laravel insights page. - closes [TET-486: Add switch on top of the table](https://linear.app/getsentry/issue/TET-486/add-switch-on-top-of-the-table)
1 parent 4cc7cb3 commit 84dbaa5

File tree

5 files changed

+337
-4
lines changed

5 files changed

+337
-4
lines changed
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export type LaravelInsightsEventParameters = {
22
'laravel-insights.page-view': Record<string, unknown>;
3-
'laravel-insights.ui_toggle': {
4-
isEnabled: boolean;
3+
'laravel-insights.table_view_change': {
4+
view: string;
55
};
66
};
77

@@ -10,5 +10,5 @@ export const laravelInsightsEventMap: Record<
1010
string
1111
> = {
1212
'laravel-insights.page-view': 'Laravel Insights: Page View',
13-
'laravel-insights.ui_toggle': 'Laravel Insights: UI Toggle',
13+
'laravel-insights.table_view_change': 'Laravel Insights: Table View Change',
1414
};
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import {useCallback} from 'react';
2+
3+
import {
4+
COL_WIDTH_UNDEFINED,
5+
type GridColumnHeader,
6+
type GridColumnOrder,
7+
} from 'sentry/components/gridEditable';
8+
import Link from 'sentry/components/links/link';
9+
import {t} from 'sentry/locale';
10+
import useOrganization from 'sentry/utils/useOrganization';
11+
import usePageFilters from 'sentry/utils/usePageFilters';
12+
import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
13+
import {getExploreUrl} from 'sentry/views/explore/utils';
14+
import {HeadSortCell} from 'sentry/views/insights/agentMonitoring/components/headSortCell';
15+
import {ChartType} from 'sentry/views/insights/common/components/chart';
16+
import {TimeSpentCell} from 'sentry/views/insights/common/components/tableCells/timeSpentCell';
17+
import {Referrer} from 'sentry/views/insights/pages/platform/laravel/referrers';
18+
import {PlatformInsightsTable} from 'sentry/views/insights/pages/platform/shared/table';
19+
import {DurationCell} from 'sentry/views/insights/pages/platform/shared/table/DurationCell';
20+
import {ErrorRateCell} from 'sentry/views/insights/pages/platform/shared/table/ErrorRateCell';
21+
import {NumberCell} from 'sentry/views/insights/pages/platform/shared/table/NumberCell';
22+
import {useTableData} from 'sentry/views/insights/pages/platform/shared/table/useTableData';
23+
24+
const defaultColumnOrder: Array<GridColumnOrder<string>> = [
25+
{key: 'command', name: t('Command Name'), width: COL_WIDTH_UNDEFINED},
26+
{key: 'count()', name: t('Invocations'), width: 136},
27+
{key: 'failure_rate()', name: t('Error Rate'), width: 124},
28+
{key: 'avg(span.duration)', name: t('AVG'), width: 90},
29+
{key: 'p95(span.duration)', name: t('P95'), width: 90},
30+
{key: 'sum(span.duration)', name: t('Time Spent'), width: 120},
31+
];
32+
33+
const rightAlignColumns = new Set([
34+
'count()',
35+
'failure_rate()',
36+
'sum(span.duration)',
37+
'avg(span.duration)',
38+
'p95(span.duration)',
39+
]);
40+
41+
export function CommandsTable() {
42+
const tableDataRequest = useTableData({
43+
query: 'span.op:console.command*',
44+
fields: [
45+
'command',
46+
'project.id',
47+
'count()',
48+
'failure_rate()',
49+
'avg(span.duration)',
50+
'p95(span.duration)',
51+
'sum(span.duration)',
52+
],
53+
cursorParamName: 'jobsCursor',
54+
referrer: Referrer.PATHS_TABLE,
55+
});
56+
57+
const renderHeadCell = useCallback((column: GridColumnHeader<string>) => {
58+
return (
59+
<HeadSortCell
60+
sortKey={column.key}
61+
align={rightAlignColumns.has(column.key) ? 'right' : 'left'}
62+
forceCellGrow={column.key === 'command'}
63+
cursorParamName="commandsCursor"
64+
>
65+
{column.name}
66+
</HeadSortCell>
67+
);
68+
}, []);
69+
70+
const renderBodyCell = useCallback(
71+
(
72+
column: GridColumnOrder<string>,
73+
dataRow: (typeof tableDataRequest.data)[number]
74+
) => {
75+
switch (column.key) {
76+
case 'command':
77+
return <CommandCell command={dataRow.command} />;
78+
case 'failure_rate()':
79+
return <ErrorRateCell errorRate={dataRow['failure_rate()']} />;
80+
case 'avg(span.duration)':
81+
case 'p95(span.duration)':
82+
return <DurationCell milliseconds={dataRow[column.key]} />;
83+
case 'count()':
84+
return <NumberCell value={dataRow['count()']} />;
85+
case 'sum(span.duration)':
86+
return <TimeSpentCell total={dataRow['sum(span.duration)']} />;
87+
default:
88+
return <div />;
89+
}
90+
},
91+
[tableDataRequest]
92+
);
93+
94+
return (
95+
<PlatformInsightsTable
96+
isLoading={tableDataRequest.isPending}
97+
error={tableDataRequest.error}
98+
data={tableDataRequest.data}
99+
initialColumnOrder={defaultColumnOrder}
100+
stickyHeader
101+
grid={{
102+
renderBodyCell,
103+
renderHeadCell,
104+
}}
105+
cursorParamName="commandsCursor"
106+
pageLinks={tableDataRequest.pageLinks}
107+
isPlaceholderData={tableDataRequest.isPlaceholderData}
108+
/>
109+
);
110+
}
111+
112+
function CommandCell({command}: {command: string}) {
113+
const organization = useOrganization();
114+
const {selection} = usePageFilters();
115+
116+
const link = getExploreUrl({
117+
organization,
118+
selection,
119+
mode: Mode.SAMPLES,
120+
visualize: [
121+
{
122+
chartType: ChartType.BAR,
123+
yAxes: ['count(span.duration)'],
124+
},
125+
],
126+
query: `span.op:console.command* command:${command}`,
127+
sort: `-count(span.duration)`,
128+
});
129+
return <Link to={link}>{command}</Link>;
130+
}

static/app/views/insights/pages/platform/laravel/index.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,51 @@
11
import {useEffect} from 'react';
2+
import styled from '@emotion/styled';
23

4+
import {SegmentedControl} from 'sentry/components/core/segmentedControl';
5+
import {t} from 'sentry/locale';
6+
import {space} from 'sentry/styles/space';
37
import {trackAnalytics} from 'sentry/utils/analytics';
8+
import {useLocation} from 'sentry/utils/useLocation';
9+
import {useNavigate} from 'sentry/utils/useNavigate';
410
import useOrganization from 'sentry/utils/useOrganization';
511
import OverviewApiLatencyChartWidget from 'sentry/views/insights/common/components/widgets/overviewApiLatencyChartWidget';
612
import OverviewCacheMissChartWidget from 'sentry/views/insights/common/components/widgets/overviewCacheMissChartWidget';
713
import OverviewJobsChartWidget from 'sentry/views/insights/common/components/widgets/overviewJobsChartWidget';
814
import OverviewRequestsChartWidget from 'sentry/views/insights/common/components/widgets/overviewRequestsChartWidget';
915
import OverviewSlowQueriesChartWidget from 'sentry/views/insights/common/components/widgets/overviewSlowQueriesChartWidget';
16+
import {CommandsTable} from 'sentry/views/insights/pages/platform/laravel/commandsTable';
17+
import {JobsTable} from 'sentry/views/insights/pages/platform/laravel/jobsTable';
1018
import {IssuesWidget} from 'sentry/views/insights/pages/platform/shared/issuesWidget';
1119
import {PlatformLandingPageLayout} from 'sentry/views/insights/pages/platform/shared/layout';
1220
import {PathsTable} from 'sentry/views/insights/pages/platform/shared/pathsTable';
1321
import {WidgetGrid} from 'sentry/views/insights/pages/platform/shared/styles';
1422

23+
enum TableType {
24+
PATHS = 'paths',
25+
COMMANDS = 'commands',
26+
JOBS = 'jobs',
27+
}
28+
29+
function isTableType(value: any): value is TableType {
30+
return Object.values(TableType).includes(value as TableType);
31+
}
32+
33+
const decodeTableType = (value: any): TableType => {
34+
if (isTableType(value)) {
35+
return value;
36+
}
37+
return TableType.PATHS;
38+
};
39+
40+
const TableControl = SegmentedControl<TableType>;
41+
const TableControlItem = SegmentedControl.Item<TableType>;
42+
1543
export function LaravelOverviewPage() {
1644
const organization = useOrganization();
45+
const location = useLocation();
46+
const navigate = useNavigate();
47+
48+
const currentView = decodeTableType(location.query.view);
1749

1850
useEffect(() => {
1951
trackAnalytics('laravel-insights.page-view', {
@@ -22,6 +54,31 @@ export function LaravelOverviewPage() {
2254
// eslint-disable-next-line react-hooks/exhaustive-deps
2355
}, []);
2456

57+
function handleViewChange(view: TableType) {
58+
trackAnalytics('laravel-insights.table_view_change', {
59+
organization,
60+
view,
61+
});
62+
63+
navigate(
64+
{
65+
pathname: location.pathname,
66+
query: {
67+
...location.query,
68+
// Reset cursors when view changes
69+
pathsCursor: undefined,
70+
commandsCursor: undefined,
71+
jobsCursor: undefined,
72+
view,
73+
},
74+
},
75+
{
76+
replace: true,
77+
preventScrollReset: true,
78+
}
79+
);
80+
}
81+
2582
return (
2683
<PlatformLandingPageLayout performanceType={'backend'}>
2784
<WidgetGrid>
@@ -44,7 +101,20 @@ export function LaravelOverviewPage() {
44101
<OverviewCacheMissChartWidget />
45102
</WidgetGrid.Position6>
46103
</WidgetGrid>
47-
<PathsTable />
104+
<ControlsWrapper>
105+
<TableControl value={currentView} onChange={handleViewChange} size="sm">
106+
<TableControlItem key={TableType.PATHS}>{t('Paths')}</TableControlItem>
107+
<TableControlItem key={TableType.COMMANDS}>{t('Commands')}</TableControlItem>
108+
<TableControlItem key={TableType.JOBS}>{t('Jobs')}</TableControlItem>
109+
</TableControl>
110+
</ControlsWrapper>
111+
{currentView === TableType.JOBS && <JobsTable />}
112+
{currentView === TableType.PATHS && <PathsTable />}
113+
{currentView === TableType.COMMANDS && <CommandsTable />}
48114
</PlatformLandingPageLayout>
49115
);
50116
}
117+
118+
const ControlsWrapper = styled('div')`
119+
padding: ${space(2)} 0;
120+
`;
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import {useCallback} from 'react';
2+
3+
import {
4+
COL_WIDTH_UNDEFINED,
5+
type GridColumnHeader,
6+
type GridColumnOrder,
7+
} from 'sentry/components/gridEditable';
8+
import Link from 'sentry/components/links/link';
9+
import {t} from 'sentry/locale';
10+
import {useLocation} from 'sentry/utils/useLocation';
11+
import {HeadSortCell} from 'sentry/views/insights/agentMonitoring/components/headSortCell';
12+
import {TimeSpentCell} from 'sentry/views/insights/common/components/tableCells/timeSpentCell';
13+
import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL';
14+
import {Referrer} from 'sentry/views/insights/pages/platform/laravel/referrers';
15+
import {PlatformInsightsTable} from 'sentry/views/insights/pages/platform/shared/table';
16+
import {DurationCell} from 'sentry/views/insights/pages/platform/shared/table/DurationCell';
17+
import {ErrorRateCell} from 'sentry/views/insights/pages/platform/shared/table/ErrorRateCell';
18+
import {NumberCell} from 'sentry/views/insights/pages/platform/shared/table/NumberCell';
19+
import {useTableData} from 'sentry/views/insights/pages/platform/shared/table/useTableData';
20+
21+
const defaultColumnOrder: Array<GridColumnOrder<string>> = [
22+
{key: 'messaging.destination.name', name: t('Job Name'), width: COL_WIDTH_UNDEFINED},
23+
{key: 'count()', name: t('Processed'), width: 124},
24+
{key: 'failure_rate()', name: t('Error Rate'), width: 124},
25+
{
26+
key: 'avg(messaging.message.receive.latency)',
27+
name: t('Avg Time in Queue'),
28+
width: 164,
29+
},
30+
{
31+
key: 'avg_if(span.duration,span.op,queue.process)',
32+
name: t('Avg Processing Time'),
33+
width: 184,
34+
},
35+
{key: 'sum(span.duration)', name: t('Time Spent'), width: 120},
36+
];
37+
38+
const rightAlignColumns = new Set([
39+
'count()',
40+
'failure_rate()',
41+
'sum(span.duration)',
42+
'avg(messaging.message.receive.latency)',
43+
'avg_if(span.duration,span.op,queue.process)',
44+
]);
45+
46+
export function JobsTable() {
47+
const tableDataRequest = useTableData({
48+
query: 'span.op:queue.process',
49+
fields: [
50+
'count()',
51+
'messaging.destination.name',
52+
'avg(messaging.message.receive.latency)',
53+
'avg_if(span.duration,span.op,queue.process)',
54+
'failure_rate()',
55+
'sum(span.duration)',
56+
],
57+
cursorParamName: 'jobsCursor',
58+
referrer: Referrer.PATHS_TABLE,
59+
});
60+
61+
const renderHeadCell = useCallback((column: GridColumnHeader<string>) => {
62+
return (
63+
<HeadSortCell
64+
sortKey={column.key}
65+
align={rightAlignColumns.has(column.key) ? 'right' : 'left'}
66+
forceCellGrow={column.key === 'messaging.destination.name'}
67+
cursorParamName="jobsCursor"
68+
>
69+
{column.name}
70+
</HeadSortCell>
71+
);
72+
}, []);
73+
74+
const renderBodyCell = useCallback(
75+
(
76+
column: GridColumnOrder<string>,
77+
dataRow: (typeof tableDataRequest.data)[number]
78+
) => {
79+
switch (column.key) {
80+
case 'messaging.destination.name':
81+
return <DestinationCell destination={dataRow['messaging.destination.name']} />;
82+
case 'failure_rate()':
83+
return <ErrorRateCell errorRate={dataRow['failure_rate()']} />;
84+
case 'avg(messaging.message.receive.latency)':
85+
case 'avg_if(span.duration,span.op,queue.process)':
86+
return <DurationCell milliseconds={dataRow[column.key]} />;
87+
case 'count()':
88+
return <NumberCell value={dataRow['count()']} />;
89+
case 'sum(span.duration)':
90+
return <TimeSpentCell total={dataRow['sum(span.duration)']} />;
91+
default:
92+
return <div />;
93+
}
94+
},
95+
[tableDataRequest]
96+
);
97+
98+
return (
99+
<PlatformInsightsTable
100+
isLoading={tableDataRequest.isPending}
101+
error={tableDataRequest.error}
102+
data={tableDataRequest.data}
103+
initialColumnOrder={defaultColumnOrder}
104+
stickyHeader
105+
grid={{
106+
renderBodyCell,
107+
renderHeadCell,
108+
}}
109+
cursorParamName="jobsCursor"
110+
pageLinks={tableDataRequest.pageLinks}
111+
isPlaceholderData={tableDataRequest.isPlaceholderData}
112+
/>
113+
);
114+
}
115+
116+
function DestinationCell({destination}: {destination: string}) {
117+
const moduleURL = useModuleURL('queue');
118+
const {query} = useLocation();
119+
return (
120+
<Link
121+
to={{
122+
pathname: `${moduleURL}/destination/`,
123+
query: {
124+
...query,
125+
destination,
126+
},
127+
}}
128+
>
129+
{destination}
130+
</Link>
131+
);
132+
}

static/app/views/insights/types.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ type SpanStringFields =
160160
| 'project'
161161
| 'http.request.method'
162162
| 'messaging.destination.name'
163+
| 'command'
163164
| 'user'
164165
| 'user.display'
165166
| 'user.id'

0 commit comments

Comments
 (0)