@sentry/junior-dashboard 0.246.0 → 0.248.0
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/dist/app.js +69 -15
- package/dist/client.js +50 -50
- package/dist/index.js +69 -15
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/components/Metric.tsx +8 -13
- package/src/client/conversations/ConversationChildAgents.tsx +6 -0
- package/src/client/conversations/ConversationMeta.tsx +20 -1
- package/src/client/conversations/ConversationPage.tsx +11 -4
- package/src/client/conversations/ConversationUsageBreakdown.tsx +278 -0
- package/src/client/conversations/TelemetryMetrics.tsx +327 -180
- package/src/client/conversations/UsageCard.tsx +172 -0
- package/src/client/format.ts +1 -0
- package/src/client/types.ts +1 -0
- package/src/mock-reporting/fixtures.ts +73 -14
|
@@ -110,10 +110,16 @@ function ChildAgentRow(props: {
|
|
|
110
110
|
// A title is often a short form of the task. Skip the task when it adds nothing.
|
|
111
111
|
const detail = agent.title && task && task !== agent.title ? task : undefined;
|
|
112
112
|
const runtime = formatRuntime(agent.cumulativeDurationMs);
|
|
113
|
+
// Totals include this subagent's own subagents. Say so, so the row does not
|
|
114
|
+
// read as the work of this subagent alone.
|
|
115
|
+
const nested = agent.childAgentTotals?.count;
|
|
113
116
|
const stats = [
|
|
114
117
|
formatUsageTotal(agent.cumulativeUsage),
|
|
115
118
|
formatConversationCostTotal(agent.cumulativeUsage, agent.auxiliaryCosts),
|
|
116
119
|
runtime ? `${runtime} runtime` : "",
|
|
120
|
+
nested
|
|
121
|
+
? `incl. ${nested} nested ${nested === 1 ? "subagent" : "subagents"}`
|
|
122
|
+
: "",
|
|
117
123
|
].filter(Boolean);
|
|
118
124
|
|
|
119
125
|
return (
|
|
@@ -25,7 +25,12 @@ import {
|
|
|
25
25
|
import { Tooltip } from "../components/Tooltip";
|
|
26
26
|
import { MetricList, type MetricListItem } from "../components/Metric";
|
|
27
27
|
import { cn } from "../styles";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
CostMetric,
|
|
30
|
+
DurationMetric,
|
|
31
|
+
SubagentsMetric,
|
|
32
|
+
TokenMetric,
|
|
33
|
+
} from "./TelemetryMetrics";
|
|
29
34
|
import type { Conversation } from "../types";
|
|
30
35
|
import {
|
|
31
36
|
projectSidebarAnnotationBadges,
|
|
@@ -598,6 +603,8 @@ function conversationStatItems(props: {
|
|
|
598
603
|
const live = (props.detail?.status ?? props.conversation.status) === "active";
|
|
599
604
|
const activeModelId = liveModelId(props.detail);
|
|
600
605
|
const sourceTask = props.detail?.sourceTask;
|
|
606
|
+
const childAgentTotals =
|
|
607
|
+
props.detail?.childAgentTotals ?? props.conversation.childAgentTotals;
|
|
601
608
|
const rawStats: Array<MetricListItem | undefined> = [
|
|
602
609
|
location
|
|
603
610
|
? {
|
|
@@ -615,6 +622,8 @@ function conversationStatItems(props: {
|
|
|
615
622
|
? {
|
|
616
623
|
content: (
|
|
617
624
|
<DurationMetric
|
|
625
|
+
childAgentTotals={childAgentTotals}
|
|
626
|
+
durationMs={props.conversation.cumulativeDurationMs}
|
|
618
627
|
endedAt={props.conversation.lastSeenAt}
|
|
619
628
|
label={durationLabel}
|
|
620
629
|
startedAt={props.conversation.startedAt}
|
|
@@ -627,6 +636,8 @@ function conversationStatItems(props: {
|
|
|
627
636
|
? {
|
|
628
637
|
content: (
|
|
629
638
|
<TokenMetric
|
|
639
|
+
detailsHint={variant === "compact"}
|
|
640
|
+
childAgentTotals={childAgentTotals}
|
|
630
641
|
compactionCount={
|
|
631
642
|
completeDetail
|
|
632
643
|
? completeDetail.events.filter(
|
|
@@ -650,10 +661,12 @@ function conversationStatItems(props: {
|
|
|
650
661
|
? {
|
|
651
662
|
content: (
|
|
652
663
|
<CostMetric
|
|
664
|
+
detailsHint={variant === "compact"}
|
|
653
665
|
auxiliaryCosts={
|
|
654
666
|
props.detail?.auxiliaryCosts ??
|
|
655
667
|
props.conversation.auxiliaryCosts
|
|
656
668
|
}
|
|
669
|
+
childAgentTotals={childAgentTotals}
|
|
657
670
|
live={live}
|
|
658
671
|
liveModelId={activeModelId}
|
|
659
672
|
modelUsage={props.detail?.modelUsage}
|
|
@@ -663,6 +676,12 @@ function conversationStatItems(props: {
|
|
|
663
676
|
key: "cost",
|
|
664
677
|
}
|
|
665
678
|
: undefined,
|
|
679
|
+
childAgentTotals
|
|
680
|
+
? {
|
|
681
|
+
content: <SubagentsMetric childAgentTotals={childAgentTotals} />,
|
|
682
|
+
key: "subagents",
|
|
683
|
+
}
|
|
684
|
+
: undefined,
|
|
666
685
|
];
|
|
667
686
|
return rawStats.filter((item): item is MetricListItem => item !== undefined);
|
|
668
687
|
}
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
hasConversationStats,
|
|
38
38
|
PendingAuthorization,
|
|
39
39
|
} from "./ConversationMeta";
|
|
40
|
+
import { ConversationUsageBreakdown } from "./ConversationUsageBreakdown";
|
|
40
41
|
import { PendingMailboxStack } from "./PendingMailboxStack";
|
|
41
42
|
import {
|
|
42
43
|
buildConversations,
|
|
@@ -300,10 +301,16 @@ export function ConversationPage(props: {
|
|
|
300
301
|
conversation,
|
|
301
302
|
detail: detail.data,
|
|
302
303
|
}) ? (
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
304
|
+
<div className="grid min-w-0 gap-5">
|
|
305
|
+
<ConversationStats
|
|
306
|
+
conversation={conversation}
|
|
307
|
+
detail={detail.data}
|
|
308
|
+
/>
|
|
309
|
+
<ConversationUsageBreakdown
|
|
310
|
+
conversation={conversation}
|
|
311
|
+
detail={detail.data}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
307
314
|
) : null
|
|
308
315
|
}
|
|
309
316
|
title={title}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConversationDetailReport,
|
|
3
|
+
ConversationModelUsage,
|
|
4
|
+
} from "@sentry/junior/api/schema";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
formatCompactNumber,
|
|
9
|
+
summarizeCost,
|
|
10
|
+
summarizeModelCost,
|
|
11
|
+
summarizeModelUsage,
|
|
12
|
+
summarizeUsage,
|
|
13
|
+
totalConversationCost,
|
|
14
|
+
} from "../format";
|
|
15
|
+
import { ShimmerText } from "../components/ShimmerText";
|
|
16
|
+
import type { Conversation } from "../types";
|
|
17
|
+
import { liveModelId } from "./ConversationMeta";
|
|
18
|
+
import {
|
|
19
|
+
auxiliaryOperationLabel,
|
|
20
|
+
costFormatter,
|
|
21
|
+
modelLabel,
|
|
22
|
+
subagentShare,
|
|
23
|
+
subagentUsage,
|
|
24
|
+
tokenTypeRows,
|
|
25
|
+
} from "./TelemetryMetrics";
|
|
26
|
+
|
|
27
|
+
type BreakdownRow = {
|
|
28
|
+
cost: string;
|
|
29
|
+
count: string;
|
|
30
|
+
/** Muted second line, such as the token types of one model. */
|
|
31
|
+
detail?: string;
|
|
32
|
+
key: string;
|
|
33
|
+
label: ReactNode;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const NOT_RECORDED = "—";
|
|
37
|
+
|
|
38
|
+
/** Name each recorded token type, such as `42k input · 4.6k output`. */
|
|
39
|
+
function tokenTypes(
|
|
40
|
+
usage: ConversationModelUsage["usage"],
|
|
41
|
+
): string | undefined {
|
|
42
|
+
const summary = summarizeUsage(usage);
|
|
43
|
+
const text = summary
|
|
44
|
+
? tokenTypeRows(summary)
|
|
45
|
+
.map((row) => `${row.value} ${row.label}`)
|
|
46
|
+
.join(" · ")
|
|
47
|
+
: "";
|
|
48
|
+
return text || undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One usage table. Every table in the breakdown shares column widths, so the
|
|
53
|
+
* cost column lines up from table to table.
|
|
54
|
+
*/
|
|
55
|
+
function BreakdownTable(props: {
|
|
56
|
+
/** Header of the count column, such as Tokens or Calls. */
|
|
57
|
+
countLabel: string;
|
|
58
|
+
label: string;
|
|
59
|
+
rows: BreakdownRow[];
|
|
60
|
+
total?: { cost: string; count: string; label: string };
|
|
61
|
+
}) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="min-w-0 overflow-x-auto">
|
|
64
|
+
<table
|
|
65
|
+
aria-label={props.label}
|
|
66
|
+
className="w-full min-w-72 table-fixed border-collapse text-xs leading-relaxed"
|
|
67
|
+
>
|
|
68
|
+
<colgroup>
|
|
69
|
+
<col />
|
|
70
|
+
<col className="w-20" />
|
|
71
|
+
<col className="w-20" />
|
|
72
|
+
</colgroup>
|
|
73
|
+
<thead>
|
|
74
|
+
<tr className="border-b border-dashboard-border text-dashboard-text-muted">
|
|
75
|
+
<th scope="col" className="py-2 text-left font-normal">
|
|
76
|
+
{props.label}
|
|
77
|
+
</th>
|
|
78
|
+
<th scope="col" className="py-2 text-right font-normal">
|
|
79
|
+
{props.countLabel}
|
|
80
|
+
</th>
|
|
81
|
+
<th scope="col" className="py-2 text-right font-normal">
|
|
82
|
+
Cost
|
|
83
|
+
</th>
|
|
84
|
+
</tr>
|
|
85
|
+
</thead>
|
|
86
|
+
<tbody>
|
|
87
|
+
{props.rows.map((row) => (
|
|
88
|
+
<tr
|
|
89
|
+
className="border-b border-dashboard-border-subtle last:border-b-0"
|
|
90
|
+
key={row.key}
|
|
91
|
+
>
|
|
92
|
+
<th scope="row" className="py-2 pr-3 text-left font-normal">
|
|
93
|
+
<span className="block truncate text-dashboard-text">
|
|
94
|
+
{row.label}
|
|
95
|
+
</span>
|
|
96
|
+
{row.detail ? (
|
|
97
|
+
<span className="mt-0.5 block text-dashboard-text-faint">
|
|
98
|
+
{row.detail}
|
|
99
|
+
</span>
|
|
100
|
+
) : null}
|
|
101
|
+
</th>
|
|
102
|
+
<td className="py-2 text-right align-top font-mono tabular-nums text-dashboard-text">
|
|
103
|
+
{row.count}
|
|
104
|
+
</td>
|
|
105
|
+
<td className="py-2 text-right align-top font-mono tabular-nums text-dashboard-text">
|
|
106
|
+
{row.cost}
|
|
107
|
+
</td>
|
|
108
|
+
</tr>
|
|
109
|
+
))}
|
|
110
|
+
{props.total ? (
|
|
111
|
+
<tr className="border-t border-dashboard-border font-semibold text-dashboard-text">
|
|
112
|
+
<th scope="row" className="py-2 text-left font-semibold">
|
|
113
|
+
{props.total.label}
|
|
114
|
+
</th>
|
|
115
|
+
<td className="py-2 text-right font-mono tabular-nums">
|
|
116
|
+
{props.total.count}
|
|
117
|
+
</td>
|
|
118
|
+
<td className="py-2 text-right font-mono tabular-nums">
|
|
119
|
+
{props.total.cost}
|
|
120
|
+
</td>
|
|
121
|
+
</tr>
|
|
122
|
+
) : null}
|
|
123
|
+
</tbody>
|
|
124
|
+
</table>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Break a Conversation's usage down by agent, model, and auxiliary
|
|
131
|
+
* operation. The header tooltips show only the largest items; this lists
|
|
132
|
+
* every one with its token types. Render nothing without model or auxiliary
|
|
133
|
+
* usage.
|
|
134
|
+
*/
|
|
135
|
+
export function ConversationUsageBreakdown(props: {
|
|
136
|
+
conversation: Conversation | undefined;
|
|
137
|
+
detail?: ConversationDetailReport;
|
|
138
|
+
}) {
|
|
139
|
+
if (!props.conversation) return null;
|
|
140
|
+
const modelUsage = props.detail?.modelUsage ?? [];
|
|
141
|
+
const auxiliaryCosts =
|
|
142
|
+
props.detail?.auxiliaryCosts ?? props.conversation.auxiliaryCosts;
|
|
143
|
+
const usage =
|
|
144
|
+
props.detail?.cumulativeUsage ?? props.conversation.cumulativeUsage;
|
|
145
|
+
const childAgentTotals =
|
|
146
|
+
props.detail?.childAgentTotals ?? props.conversation.childAgentTotals;
|
|
147
|
+
const tokens =
|
|
148
|
+
summarizeModelUsage(modelUsage)?.totalTokens ??
|
|
149
|
+
summarizeUsage(usage)?.totalTokens;
|
|
150
|
+
const modelCost = summarizeModelCost(modelUsage) ?? summarizeCost(usage);
|
|
151
|
+
const cost = totalConversationCost(modelCost, auxiliaryCosts)?.total;
|
|
152
|
+
const operations = [...(auxiliaryCosts?.operations ?? [])].sort(
|
|
153
|
+
(a, b) => b.costUsd - a.costUsd,
|
|
154
|
+
);
|
|
155
|
+
if (!modelUsage.length && !operations.length) return null;
|
|
156
|
+
|
|
157
|
+
const formatCost = costFormatter(cost ?? 0);
|
|
158
|
+
const formatTokens = (value: number | undefined) =>
|
|
159
|
+
value === undefined ? NOT_RECORDED : formatCompactNumber(value);
|
|
160
|
+
const formatMaybeCost = (value: number | undefined) =>
|
|
161
|
+
value === undefined ? NOT_RECORDED : formatCost(value);
|
|
162
|
+
const activeModelId = liveModelId(props.detail);
|
|
163
|
+
|
|
164
|
+
const models = modelUsage
|
|
165
|
+
.map((item) => ({
|
|
166
|
+
cost: summarizeCost(item.usage)?.total,
|
|
167
|
+
item,
|
|
168
|
+
tokens: summarizeUsage(item.usage)?.totalTokens,
|
|
169
|
+
}))
|
|
170
|
+
.sort(
|
|
171
|
+
(a, b) =>
|
|
172
|
+
(b.cost ?? 0) - (a.cost ?? 0) || (b.tokens ?? 0) - (a.tokens ?? 0),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const subagents = subagentUsage(childAgentTotals);
|
|
176
|
+
const subagentCost = subagentShare(subagents.cost, cost ?? subagents.cost);
|
|
177
|
+
const subagentTokens = subagentShare(
|
|
178
|
+
subagents.tokens,
|
|
179
|
+
tokens ?? subagents.tokens,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className="grid min-w-0 gap-5">
|
|
184
|
+
{childAgentTotals ? (
|
|
185
|
+
<BreakdownTable
|
|
186
|
+
countLabel="Tokens"
|
|
187
|
+
label="Agent"
|
|
188
|
+
rows={[
|
|
189
|
+
{
|
|
190
|
+
cost: formatMaybeCost(
|
|
191
|
+
cost === undefined ? undefined : cost - subagentCost,
|
|
192
|
+
),
|
|
193
|
+
count: formatTokens(
|
|
194
|
+
tokens === undefined ? undefined : tokens - subagentTokens,
|
|
195
|
+
),
|
|
196
|
+
key: "self",
|
|
197
|
+
label: "This agent",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
cost: formatCost(subagentCost),
|
|
201
|
+
count: formatTokens(subagentTokens),
|
|
202
|
+
key: "subagents",
|
|
203
|
+
label: `${formatCompactNumber(childAgentTotals.count)} subagent${childAgentTotals.count === 1 ? "" : "s"}`,
|
|
204
|
+
},
|
|
205
|
+
]}
|
|
206
|
+
total={{
|
|
207
|
+
cost: formatMaybeCost(cost),
|
|
208
|
+
count: formatTokens(tokens),
|
|
209
|
+
label: "Total",
|
|
210
|
+
}}
|
|
211
|
+
/>
|
|
212
|
+
) : null}
|
|
213
|
+
{models.length ? (
|
|
214
|
+
<BreakdownTable
|
|
215
|
+
countLabel="Tokens"
|
|
216
|
+
label="Model"
|
|
217
|
+
rows={models.map(({ cost: itemCost, item, tokens: itemTokens }) => ({
|
|
218
|
+
cost: formatMaybeCost(itemCost),
|
|
219
|
+
count: formatTokens(itemTokens),
|
|
220
|
+
detail: tokenTypes(item.usage),
|
|
221
|
+
key: item.modelId,
|
|
222
|
+
label: (
|
|
223
|
+
<span className="flex min-w-0 items-baseline gap-2">
|
|
224
|
+
<span
|
|
225
|
+
className="min-w-0 truncate font-mono"
|
|
226
|
+
title={item.modelId}
|
|
227
|
+
>
|
|
228
|
+
{modelLabel(item.modelId)}
|
|
229
|
+
</span>
|
|
230
|
+
{item.modelId === activeModelId ? (
|
|
231
|
+
<span className="shrink-0 text-dashboard-text-muted">
|
|
232
|
+
<ShimmerText active>in progress</ShimmerText>
|
|
233
|
+
</span>
|
|
234
|
+
) : null}
|
|
235
|
+
</span>
|
|
236
|
+
),
|
|
237
|
+
}))}
|
|
238
|
+
total={
|
|
239
|
+
models.length > 1
|
|
240
|
+
? {
|
|
241
|
+
cost: formatMaybeCost(modelCost?.total),
|
|
242
|
+
count: formatTokens(tokens),
|
|
243
|
+
label: "All models",
|
|
244
|
+
}
|
|
245
|
+
: undefined
|
|
246
|
+
}
|
|
247
|
+
/>
|
|
248
|
+
) : null}
|
|
249
|
+
{auxiliaryCosts && operations.length ? (
|
|
250
|
+
<BreakdownTable
|
|
251
|
+
countLabel="Calls"
|
|
252
|
+
label="Auxiliary"
|
|
253
|
+
rows={operations.map((operation) => ({
|
|
254
|
+
cost: formatCost(operation.costUsd),
|
|
255
|
+
count: formatCompactNumber(operation.events),
|
|
256
|
+
key: `${operation.namespace}/${operation.name}`,
|
|
257
|
+
label: auxiliaryOperationLabel(operation),
|
|
258
|
+
}))}
|
|
259
|
+
total={
|
|
260
|
+
operations.length > 1
|
|
261
|
+
? {
|
|
262
|
+
cost: formatCost(auxiliaryCosts.costUsd),
|
|
263
|
+
count: formatCompactNumber(
|
|
264
|
+
operations.reduce((sum, item) => sum + item.events, 0),
|
|
265
|
+
),
|
|
266
|
+
label: "All auxiliary",
|
|
267
|
+
}
|
|
268
|
+
: undefined
|
|
269
|
+
}
|
|
270
|
+
/>
|
|
271
|
+
) : null}
|
|
272
|
+
<p className="m-0 text-xs leading-relaxed text-dashboard-text-muted">
|
|
273
|
+
Estimated costs.
|
|
274
|
+
{childAgentTotals ? " Totals include subagents at any depth." : null}
|
|
275
|
+
</p>
|
|
276
|
+
</div>
|
|
277
|
+
);
|
|
278
|
+
}
|