@sentry/junior-dashboard 0.91.0 → 0.92.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 +41 -44
- package/dist/client/api.d.ts +4 -4
- package/dist/client/components/ConversationListControls.d.ts +3 -3
- package/dist/client/format.d.ts +11 -11
- package/dist/client/pages/PeoplePage.d.ts +6 -6
- package/dist/client/types.d.ts +9 -9
- package/dist/client.js +6 -6
- package/dist/index.js +41 -44
- package/dist/mock-reporting/conversation.d.ts +2 -2
- package/package.json +3 -3
- package/src/app.ts +1 -1
- package/src/client/api.ts +12 -12
- package/src/client/components/ConversationDurationChart.tsx +3 -3
- package/src/client/components/ConversationListControls.tsx +9 -11
- package/src/client/components/ConversationSummary.tsx +3 -3
- package/src/client/components/TranscriptTurn.tsx +2 -2
- package/src/client/format.ts +34 -34
- package/src/client/markdownExport.ts +5 -13
- package/src/client/pages/ConversationPage.tsx +3 -3
- package/src/client/pages/ConversationsPage.tsx +9 -9
- package/src/client/pages/PeoplePage.tsx +43 -52
- package/src/client/types.ts +13 -13
- package/src/mock-conversations.ts +37 -41
- package/src/mock-release-conversation.ts +3 -3
- package/src/mock-reporting/conversation.ts +4 -4
|
@@ -2,7 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
import { Link, useParams } from "react-router";
|
|
3
3
|
import { Mail, UserRound } from "lucide-react";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { useActorDirectoryData, useActorProfileData } from "../api";
|
|
6
6
|
import { Button } from "../components/Button";
|
|
7
7
|
import { ConversationList } from "../components/ConversationList";
|
|
8
8
|
import { ConversationSearchInput } from "../components/ConversationListControls";
|
|
@@ -23,37 +23,31 @@ import {
|
|
|
23
23
|
import { cn } from "../styles";
|
|
24
24
|
import type {
|
|
25
25
|
ConversationStatsItem,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
ActorActivityDay,
|
|
27
|
+
ActorDirectory,
|
|
28
|
+
ActorProfile,
|
|
29
|
+
ActorSummary,
|
|
30
|
+
ActorTotals,
|
|
31
31
|
} from "../types";
|
|
32
32
|
|
|
33
33
|
type PeopleSort = "activeDays" | "conversations" | "recent" | "runtime";
|
|
34
34
|
|
|
35
|
-
function
|
|
35
|
+
function actorName(person: Pick<ActorSummary, "actor">): string {
|
|
36
36
|
return (
|
|
37
|
-
person.
|
|
38
|
-
person.requester.slackUserName ??
|
|
39
|
-
person.requester.email
|
|
37
|
+
person.actor.fullName ?? person.actor.slackUserName ?? person.actor.email
|
|
40
38
|
);
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
function personMeta(person:
|
|
41
|
+
function personMeta(person: ActorSummary): string {
|
|
44
42
|
const pieces = [
|
|
45
|
-
person.
|
|
46
|
-
person.
|
|
47
|
-
? `@${person.requester.slackUserName}`
|
|
48
|
-
: undefined,
|
|
43
|
+
person.actor.email,
|
|
44
|
+
person.actor.slackUserName ? `@${person.actor.slackUserName}` : undefined,
|
|
49
45
|
`last ${formatRelativeTime(person.lastSeenAt)}`,
|
|
50
46
|
];
|
|
51
47
|
return pieces.filter(Boolean).join(" / ");
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
function sampleLabel(
|
|
55
|
-
data: Pick<RequesterDirectory, "sampleSize" | "truncated">,
|
|
56
|
-
) {
|
|
50
|
+
function sampleLabel(data: Pick<ActorDirectory, "sampleSize" | "truncated">) {
|
|
57
51
|
return `${formatCompactNumber(data.sampleSize)} sampled conversations${
|
|
58
52
|
data.truncated ? " / limited sample" : ""
|
|
59
53
|
}`;
|
|
@@ -64,18 +58,18 @@ function runtimeLabel(durationMs: number, conversations: number): string {
|
|
|
64
58
|
return formatMs(durationMs);
|
|
65
59
|
}
|
|
66
60
|
|
|
67
|
-
/** Render the
|
|
61
|
+
/** Render the actor directory from dashboard reporting data. */
|
|
68
62
|
export function PeoplePage() {
|
|
69
|
-
const query =
|
|
63
|
+
const query = useActorDirectoryData();
|
|
70
64
|
return <PeoplePageContent data={query.data} error={query.error} />;
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
/** Render loaded, failed, and empty
|
|
67
|
+
/** Render loaded, failed, and empty actor directory states. */
|
|
74
68
|
export function PeoplePageContent({
|
|
75
69
|
data,
|
|
76
70
|
error,
|
|
77
71
|
}: {
|
|
78
|
-
data:
|
|
72
|
+
data: ActorDirectory | undefined;
|
|
79
73
|
error: unknown;
|
|
80
74
|
}) {
|
|
81
75
|
const [peopleSearch, setPeopleSearch] = useState("");
|
|
@@ -92,7 +86,7 @@ export function PeoplePageContent({
|
|
|
92
86
|
<SectionTitle>People</SectionTitle>
|
|
93
87
|
<div className="mt-1 break-words text-[0.82rem] leading-relaxed text-[#b8b8b8]">
|
|
94
88
|
{data
|
|
95
|
-
? `${people.length} of ${data.people.length}
|
|
89
|
+
? `${people.length} of ${data.people.length} actors / ${sampleLabel(data)}`
|
|
96
90
|
: "People failed to load"}
|
|
97
91
|
</div>
|
|
98
92
|
</div>
|
|
@@ -116,7 +110,7 @@ export function PeoplePageContent({
|
|
|
116
110
|
) : (
|
|
117
111
|
<div className="p-3">
|
|
118
112
|
<EmptyTelemetry>
|
|
119
|
-
No
|
|
113
|
+
No actor telemetry with trusted email.
|
|
120
114
|
</EmptyTelemetry>
|
|
121
115
|
</div>
|
|
122
116
|
)}
|
|
@@ -126,18 +120,18 @@ export function PeoplePageContent({
|
|
|
126
120
|
}
|
|
127
121
|
|
|
128
122
|
function filterPeople(
|
|
129
|
-
people:
|
|
123
|
+
people: ActorSummary[],
|
|
130
124
|
query: string,
|
|
131
125
|
sort: PeopleSort,
|
|
132
|
-
):
|
|
126
|
+
): ActorSummary[] {
|
|
133
127
|
const normalized = query.trim().toLowerCase();
|
|
134
128
|
const filtered = normalized
|
|
135
129
|
? people.filter((person) =>
|
|
136
130
|
[
|
|
137
|
-
person.
|
|
138
|
-
person.
|
|
139
|
-
person.
|
|
140
|
-
person.
|
|
131
|
+
person.actor.email,
|
|
132
|
+
person.actor.fullName,
|
|
133
|
+
person.actor.slackUserId,
|
|
134
|
+
person.actor.slackUserName,
|
|
141
135
|
]
|
|
142
136
|
.filter(Boolean)
|
|
143
137
|
.join(" ")
|
|
@@ -196,7 +190,7 @@ function PeopleToolbar(props: {
|
|
|
196
190
|
);
|
|
197
191
|
}
|
|
198
192
|
|
|
199
|
-
function PeopleDirectory(props: { people:
|
|
193
|
+
function PeopleDirectory(props: { people: ActorSummary[] }) {
|
|
200
194
|
if (props.people.length === 0) {
|
|
201
195
|
return (
|
|
202
196
|
<div className="p-3">
|
|
@@ -210,7 +204,7 @@ function PeopleDirectory(props: { people: RequesterSummary[] }) {
|
|
|
210
204
|
className="sticky top-0 z-[1] grid grid-cols-[minmax(14rem,1fr)_repeat(3,minmax(5rem,auto))] items-center gap-3 border-b border-white/10 bg-[#050505] px-3 py-2 text-[0.76rem] font-semibold uppercase leading-none text-[#888] max-md:hidden"
|
|
211
205
|
role="row"
|
|
212
206
|
>
|
|
213
|
-
<div>
|
|
207
|
+
<div>Actor</div>
|
|
214
208
|
<div className="justify-self-end">Conversations</div>
|
|
215
209
|
<div className="justify-self-end">Active days</div>
|
|
216
210
|
<div className="justify-self-end">Runtime</div>
|
|
@@ -218,8 +212,8 @@ function PeopleDirectory(props: { people: RequesterSummary[] }) {
|
|
|
218
212
|
{props.people.map((person) => (
|
|
219
213
|
<Link
|
|
220
214
|
className="grid min-w-0 grid-cols-[minmax(14rem,1fr)_repeat(3,minmax(5rem,auto))] items-center gap-3 border-b border-l-4 border-b-white/10 border-l-[#22a06b] bg-[#0b0b0b] px-3 py-3 text-inherit no-underline transition-colors hover:bg-[#151515] max-md:grid-cols-1 max-md:px-4 max-md:py-4"
|
|
221
|
-
key={person.
|
|
222
|
-
to={peoplePath(person.
|
|
215
|
+
key={person.actor.email}
|
|
216
|
+
to={peoplePath(person.actor.email)}
|
|
223
217
|
>
|
|
224
218
|
<div className="min-w-0">
|
|
225
219
|
<div className="flex min-w-0 items-center gap-2">
|
|
@@ -228,7 +222,7 @@ function PeopleDirectory(props: { people: RequesterSummary[] }) {
|
|
|
228
222
|
</span>
|
|
229
223
|
<div className="min-w-0">
|
|
230
224
|
<div className="truncate text-[1.02rem] font-bold leading-tight text-white">
|
|
231
|
-
{
|
|
225
|
+
{actorName(person)}
|
|
232
226
|
</div>
|
|
233
227
|
<div className="mt-1 truncate text-[0.82rem] leading-tight text-[#b8b8b8]">
|
|
234
228
|
{personMeta(person)}
|
|
@@ -255,11 +249,11 @@ function DirectoryNumber(props: { value: number }) {
|
|
|
255
249
|
);
|
|
256
250
|
}
|
|
257
251
|
|
|
258
|
-
/** Render one
|
|
252
|
+
/** Render one actor's profile and recent conversation history. */
|
|
259
253
|
export function PersonProfilePage() {
|
|
260
254
|
const params = useParams();
|
|
261
255
|
const email = params.email ? decodeURIComponent(params.email) : undefined;
|
|
262
|
-
const query =
|
|
256
|
+
const query = useActorProfileData(email);
|
|
263
257
|
if (!query.data && !query.error) {
|
|
264
258
|
return <LoadingView label="Loading profile" />;
|
|
265
259
|
}
|
|
@@ -279,7 +273,7 @@ export function PersonProfilePage() {
|
|
|
279
273
|
);
|
|
280
274
|
}
|
|
281
275
|
|
|
282
|
-
export function Profile(props: { profile:
|
|
276
|
+
export function Profile(props: { profile: ActorProfile }) {
|
|
283
277
|
const profile = props.profile;
|
|
284
278
|
const [recentSearch, setRecentSearch] = useState("");
|
|
285
279
|
const conversations = buildConversations(profile.recentConversations);
|
|
@@ -297,14 +291,14 @@ export function Profile(props: { profile: RequesterProfile }) {
|
|
|
297
291
|
</span>
|
|
298
292
|
<div className="min-w-0">
|
|
299
293
|
<h2 className="m-0 truncate text-2xl font-extrabold leading-tight tracking-normal text-white">
|
|
300
|
-
{profile.
|
|
301
|
-
profile.
|
|
302
|
-
profile.
|
|
294
|
+
{profile.actor.fullName ??
|
|
295
|
+
profile.actor.slackUserName ??
|
|
296
|
+
profile.actor.email}
|
|
303
297
|
</h2>
|
|
304
298
|
<div className="mt-1 break-words text-[0.88rem] leading-relaxed text-[#b8b8b8]">
|
|
305
|
-
{profile.
|
|
306
|
-
{profile.
|
|
307
|
-
? ` / @${profile.
|
|
299
|
+
{profile.actor.email}
|
|
300
|
+
{profile.actor.slackUserName
|
|
301
|
+
? ` / @${profile.actor.slackUserName}`
|
|
308
302
|
: ""}
|
|
309
303
|
</div>
|
|
310
304
|
</div>
|
|
@@ -368,7 +362,7 @@ export function Profile(props: { profile: RequesterProfile }) {
|
|
|
368
362
|
);
|
|
369
363
|
}
|
|
370
364
|
|
|
371
|
-
function ProfileMetrics(props: { totals:
|
|
365
|
+
function ProfileMetrics(props: { totals: ActorTotals }) {
|
|
372
366
|
const totals = props.totals;
|
|
373
367
|
const metrics = [
|
|
374
368
|
["conversations", formatCompactNumber(totals.conversations)],
|
|
@@ -395,22 +389,19 @@ function ProfileMetrics(props: { totals: RequesterTotals }) {
|
|
|
395
389
|
);
|
|
396
390
|
}
|
|
397
391
|
|
|
398
|
-
function ContributionGrid(props: { days:
|
|
392
|
+
function ContributionGrid(props: { days: ActorActivityDay[] }) {
|
|
399
393
|
const max = Math.max(1, ...props.days.map((day) => day.conversations));
|
|
400
|
-
const weeks: Array<Array<
|
|
394
|
+
const weeks: Array<Array<ActorActivityDay | null>> = [];
|
|
401
395
|
const leadingDays = props.days[0]
|
|
402
396
|
? new Date(`${props.days[0].date}T00:00:00Z`).getUTCDay()
|
|
403
397
|
: 0;
|
|
404
|
-
const cells: Array<
|
|
398
|
+
const cells: Array<ActorActivityDay | null> = [
|
|
405
399
|
...Array.from({ length: leadingDays }, () => null),
|
|
406
400
|
...props.days,
|
|
407
401
|
];
|
|
408
402
|
while (cells.length > 0 && cells.length % 7 !== 0) cells.push(null);
|
|
409
403
|
for (let index = 0; index < cells.length; index += 7) {
|
|
410
|
-
const week: Array<
|
|
411
|
-
index,
|
|
412
|
-
index + 7,
|
|
413
|
-
);
|
|
404
|
+
const week: Array<ActorActivityDay | null> = cells.slice(index, index + 7);
|
|
414
405
|
while (week.length < 7) week.push(null);
|
|
415
406
|
weeks.push(week);
|
|
416
407
|
}
|
package/src/client/types.ts
CHANGED
|
@@ -16,14 +16,14 @@ import type {
|
|
|
16
16
|
SkillReport,
|
|
17
17
|
} from "@sentry/junior/reporting";
|
|
18
18
|
import type {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
ActorDirectoryReport,
|
|
20
|
+
ActorIdentity as ApiActorIdentity,
|
|
21
|
+
ActorSummaryReport,
|
|
22
|
+
ActorTotalsReport,
|
|
23
23
|
} from "@sentry/junior/api/people/list";
|
|
24
24
|
import type {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
ActorActivityDayReport,
|
|
26
|
+
ActorProfileReport,
|
|
27
27
|
} from "@sentry/junior/api/people/profile";
|
|
28
28
|
|
|
29
29
|
export type Health = HealthReport;
|
|
@@ -45,17 +45,17 @@ export type ConversationSubagentTranscript =
|
|
|
45
45
|
|
|
46
46
|
export type ConversationStatsItem = ReportingConversationStatsItem;
|
|
47
47
|
|
|
48
|
-
export type
|
|
48
|
+
export type ActorIdentity = ApiActorIdentity;
|
|
49
49
|
|
|
50
|
-
export type
|
|
50
|
+
export type ActorActivityDay = ActorActivityDayReport;
|
|
51
51
|
|
|
52
|
-
export type
|
|
52
|
+
export type ActorDirectory = ActorDirectoryReport;
|
|
53
53
|
|
|
54
|
-
export type
|
|
54
|
+
export type ActorProfile = ActorProfileReport;
|
|
55
55
|
|
|
56
|
-
export type
|
|
56
|
+
export type ActorSummary = ActorSummaryReport;
|
|
57
57
|
|
|
58
|
-
export type
|
|
58
|
+
export type ActorTotals = ActorTotalsReport;
|
|
59
59
|
|
|
60
60
|
export type TurnUsage = ConversationUsage;
|
|
61
61
|
|
|
@@ -136,7 +136,7 @@ export type Conversation = {
|
|
|
136
136
|
id: string;
|
|
137
137
|
lastProgressAt: string;
|
|
138
138
|
lastSeenAt: string;
|
|
139
|
-
|
|
139
|
+
actorIdentity?: ActorIdentity;
|
|
140
140
|
sentryTraceUrl?: string;
|
|
141
141
|
startedAt: string;
|
|
142
142
|
status: ConversationSummary["status"];
|
|
@@ -3,7 +3,7 @@ import type {
|
|
|
3
3
|
ConversationStatsItem as DashboardConversationStatsItem,
|
|
4
4
|
ConversationStatsReport as DashboardConversationStatsReport,
|
|
5
5
|
ConversationSubagentTranscriptReport as DashboardConversationSubagentTranscriptReport,
|
|
6
|
-
|
|
6
|
+
ActorIdentity as DashboardActorIdentity,
|
|
7
7
|
ConversationFeed as DashboardConversationFeed,
|
|
8
8
|
ConversationSummaryReport as DashboardConversationSummary,
|
|
9
9
|
ConversationUsage as DashboardRunUsage,
|
|
@@ -90,7 +90,7 @@ function publicIncidentConversation(
|
|
|
90
90
|
totalTokens: 9700,
|
|
91
91
|
},
|
|
92
92
|
surface: "slack",
|
|
93
|
-
|
|
93
|
+
actorIdentity: {
|
|
94
94
|
email: "avery@sentry.io",
|
|
95
95
|
fullName: "Avery Stone",
|
|
96
96
|
slackUserId: "UQA111",
|
|
@@ -189,7 +189,7 @@ function publicIncidentConversation(
|
|
|
189
189
|
totalTokens: 9250,
|
|
190
190
|
},
|
|
191
191
|
surface: "slack",
|
|
192
|
-
|
|
192
|
+
actorIdentity: {
|
|
193
193
|
email: "morgan@sentry.io",
|
|
194
194
|
fullName: "Morgan Lee",
|
|
195
195
|
slackUserId: "UQA222",
|
|
@@ -284,7 +284,7 @@ function activeConversation(nowMs: number): DashboardConversationReport {
|
|
|
284
284
|
totalTokens: 8420,
|
|
285
285
|
},
|
|
286
286
|
surface: "slack",
|
|
287
|
-
|
|
287
|
+
actorIdentity: {
|
|
288
288
|
email: "sam@sentry.io",
|
|
289
289
|
fullName: "Sam Rivera",
|
|
290
290
|
slackUserId: "UQA333",
|
|
@@ -355,7 +355,7 @@ function privateConversation(nowMs: number): DashboardConversationReport {
|
|
|
355
355
|
totalTokens: 3540,
|
|
356
356
|
},
|
|
357
357
|
surface: "slack",
|
|
358
|
-
|
|
358
|
+
actorIdentity: {
|
|
359
359
|
email: "private-user@sentry.io",
|
|
360
360
|
slackUserId: "UQA444",
|
|
361
361
|
slackUserName: "private-user",
|
|
@@ -462,7 +462,7 @@ function hungConversation(nowMs: number): DashboardConversationReport {
|
|
|
462
462
|
totalTokens: 12_000,
|
|
463
463
|
},
|
|
464
464
|
surface: "slack",
|
|
465
|
-
|
|
465
|
+
actorIdentity: {
|
|
466
466
|
email: "dana@sentry.io",
|
|
467
467
|
fullName: "Dana Chen",
|
|
468
468
|
slackUserId: "UQA555",
|
|
@@ -540,7 +540,7 @@ function failedConversation(nowMs: number): DashboardConversationReport {
|
|
|
540
540
|
totalTokens: 4890,
|
|
541
541
|
},
|
|
542
542
|
surface: "slack",
|
|
543
|
-
|
|
543
|
+
actorIdentity: {
|
|
544
544
|
email: "riley@sentry.io",
|
|
545
545
|
fullName: "Riley Patel",
|
|
546
546
|
slackUserId: "UQA666",
|
|
@@ -774,7 +774,7 @@ function dashboardQaConversation(nowMs: number): DashboardConversationReport {
|
|
|
774
774
|
name: "advisor",
|
|
775
775
|
input: {
|
|
776
776
|
question:
|
|
777
|
-
"Review the dashboard plan before editing. Focus on whether
|
|
777
|
+
"Review the dashboard plan before editing. Focus on whether actor email can be trusted, what profile metrics are useful, and what UI risks to avoid.",
|
|
778
778
|
},
|
|
779
779
|
}),
|
|
780
780
|
],
|
|
@@ -789,7 +789,7 @@ function dashboardQaConversation(nowMs: number): DashboardConversationReport {
|
|
|
789
789
|
output: {
|
|
790
790
|
verdict: "proceed",
|
|
791
791
|
summary:
|
|
792
|
-
"Use trusted
|
|
792
|
+
"Use trusted actorIdentity.email, keep metrics to conversations/runtime/tokens, and make profile activity scannable before adding heavier analytics.",
|
|
793
793
|
},
|
|
794
794
|
}),
|
|
795
795
|
],
|
|
@@ -823,7 +823,7 @@ function dashboardQaConversation(nowMs: number): DashboardConversationReport {
|
|
|
823
823
|
},
|
|
824
824
|
],
|
|
825
825
|
summary:
|
|
826
|
-
"Add
|
|
826
|
+
"Add actor activity grid, recent conversations, and email profile links.",
|
|
827
827
|
},
|
|
828
828
|
}),
|
|
829
829
|
],
|
|
@@ -846,7 +846,7 @@ function dashboardQaConversation(nowMs: number): DashboardConversationReport {
|
|
|
846
846
|
risks: {
|
|
847
847
|
auth: "dashboard routes remain authenticated",
|
|
848
848
|
privacy:
|
|
849
|
-
"
|
|
849
|
+
"actor emails are trusted from normalized reporting identity",
|
|
850
850
|
},
|
|
851
851
|
},
|
|
852
852
|
}),
|
|
@@ -911,7 +911,7 @@ function dashboardQaConversation(nowMs: number): DashboardConversationReport {
|
|
|
911
911
|
parts: [
|
|
912
912
|
{
|
|
913
913
|
type: "text",
|
|
914
|
-
text: "Implemented the people profile route, linked
|
|
914
|
+
text: "Implemented the people profile route, linked actor emails, and tightened the dashboard widgets based on the advisor review.",
|
|
915
915
|
},
|
|
916
916
|
],
|
|
917
917
|
}),
|
|
@@ -925,7 +925,7 @@ function dashboardQaConversation(nowMs: number): DashboardConversationReport {
|
|
|
925
925
|
status: "completed",
|
|
926
926
|
args: {
|
|
927
927
|
question:
|
|
928
|
-
"Review the dashboard plan before editing. Focus on whether
|
|
928
|
+
"Review the dashboard plan before editing. Focus on whether actor email can be trusted, what profile metrics are useful, and what UI risks to avoid.",
|
|
929
929
|
},
|
|
930
930
|
subagents: [
|
|
931
931
|
mockSubagentActivity({
|
|
@@ -1014,7 +1014,7 @@ function dashboardQaAdvisorTranscript(
|
|
|
1014
1014
|
parts: [
|
|
1015
1015
|
{
|
|
1016
1016
|
type: "text",
|
|
1017
|
-
text: "Review the dashboard plan before editing. Focus on whether
|
|
1017
|
+
text: "Review the dashboard plan before editing. Focus on whether actor email can be trusted, what profile metrics are useful, and what UI risks to avoid.",
|
|
1018
1018
|
},
|
|
1019
1019
|
],
|
|
1020
1020
|
}),
|
|
@@ -1024,7 +1024,7 @@ function dashboardQaAdvisorTranscript(
|
|
|
1024
1024
|
parts: [
|
|
1025
1025
|
{
|
|
1026
1026
|
type: "text",
|
|
1027
|
-
text: "
|
|
1027
|
+
text: "Actor identity email is a reasonable profile key because reporting already normalizes trusted identities. Keep the first cut narrow: total conversations, runtime, token volume, recent conversations, and a contribution-style activity grid. Avoid attention widgets until there is an explicit operator workflow.",
|
|
1028
1028
|
},
|
|
1029
1029
|
],
|
|
1030
1030
|
}),
|
|
@@ -1137,7 +1137,7 @@ function conversationStatsReportFromSummaries(
|
|
|
1137
1137
|
summaries: DashboardConversationSummary[],
|
|
1138
1138
|
): DashboardConversationStatsReport {
|
|
1139
1139
|
const conversations = recentConversationGroups(nowMs, summaries);
|
|
1140
|
-
const
|
|
1140
|
+
const actors = new Map<string, DashboardConversationStatsItem>();
|
|
1141
1141
|
const locations = new Map<string, DashboardConversationStatsItem>();
|
|
1142
1142
|
let durationMs = 0;
|
|
1143
1143
|
let tokens: number | undefined;
|
|
@@ -1155,29 +1155,25 @@ function conversationStatsReportFromSummaries(
|
|
|
1155
1155
|
failed += signals.failed ? 1 : 0;
|
|
1156
1156
|
hung += signals.hung ? 1 : 0;
|
|
1157
1157
|
|
|
1158
|
-
const
|
|
1158
|
+
const actorRuns = new Map<string, RunContribution[]>();
|
|
1159
1159
|
for (const contribution of contributions) {
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
requesterRuns.set(requester, [
|
|
1163
|
-
...(requesterRuns.get(requester) ?? []),
|
|
1164
|
-
contribution,
|
|
1165
|
-
]);
|
|
1160
|
+
const actor = actorLabel(contribution.run.actorIdentity) ?? "Unknown";
|
|
1161
|
+
actorRuns.set(actor, [...(actorRuns.get(actor) ?? []), contribution]);
|
|
1166
1162
|
}
|
|
1167
1163
|
|
|
1168
|
-
for (const [
|
|
1169
|
-
const item =
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1164
|
+
for (const [actor, actorContributions] of actorRuns) {
|
|
1165
|
+
const item = actors.get(actor) ?? emptyStatsItem(actor);
|
|
1166
|
+
const actorSignals = statusSignals(
|
|
1167
|
+
actorContributions.map((contribution) => contribution.run),
|
|
1172
1168
|
);
|
|
1173
1169
|
item.conversations += 1;
|
|
1174
|
-
item.runs +=
|
|
1175
|
-
item.durationMs += contributionDurationTotal(
|
|
1176
|
-
item.active +=
|
|
1177
|
-
item.failed +=
|
|
1178
|
-
item.hung +=
|
|
1179
|
-
addItemTokens(item, contributionTokenTotal(
|
|
1180
|
-
|
|
1170
|
+
item.runs += actorContributions.length;
|
|
1171
|
+
item.durationMs += contributionDurationTotal(actorContributions);
|
|
1172
|
+
item.active += actorSignals.active ? 1 : 0;
|
|
1173
|
+
item.failed += actorSignals.failed ? 1 : 0;
|
|
1174
|
+
item.hung += actorSignals.hung ? 1 : 0;
|
|
1175
|
+
addItemTokens(item, contributionTokenTotal(actorContributions));
|
|
1176
|
+
actors.set(actor, item);
|
|
1181
1177
|
}
|
|
1182
1178
|
|
|
1183
1179
|
const location = locationLabel(newestRun(runs));
|
|
@@ -1200,7 +1196,7 @@ function conversationStatsReportFromSummaries(
|
|
|
1200
1196
|
generatedAt: iso(nowMs),
|
|
1201
1197
|
hung,
|
|
1202
1198
|
locations: statsItems(locations),
|
|
1203
|
-
|
|
1199
|
+
actors: statsItems(actors),
|
|
1204
1200
|
sampleLimit: summaries.length,
|
|
1205
1201
|
sampleSize: summaries.length,
|
|
1206
1202
|
source: "conversation_index",
|
|
@@ -1332,13 +1328,13 @@ function contributionTokenTotal(
|
|
|
1332
1328
|
);
|
|
1333
1329
|
}
|
|
1334
1330
|
|
|
1335
|
-
function
|
|
1336
|
-
|
|
1331
|
+
function actorLabel(
|
|
1332
|
+
actor: DashboardActorIdentity | undefined,
|
|
1337
1333
|
): string | undefined {
|
|
1338
|
-
const email =
|
|
1339
|
-
const fullName =
|
|
1340
|
-
const slackUserName =
|
|
1341
|
-
return email ?? fullName ?? slackUserName ??
|
|
1334
|
+
const email = actor?.email?.trim() || undefined;
|
|
1335
|
+
const fullName = actor?.fullName?.trim() || undefined;
|
|
1336
|
+
const slackUserName = actor?.slackUserName?.trim() || undefined;
|
|
1337
|
+
return email ?? fullName ?? slackUserName ?? actor?.slackUserId;
|
|
1342
1338
|
}
|
|
1343
1339
|
|
|
1344
1340
|
function locationLabel(turn: DashboardConversationSummary): string {
|
|
@@ -34,7 +34,7 @@ function mockSystemPrompt(): string {
|
|
|
34
34
|
"",
|
|
35
35
|
"- Default application repo: acme/junior-demo.",
|
|
36
36
|
"- Default package namespace: @acme/junior.",
|
|
37
|
-
"- Open pull requests as drafts unless the
|
|
37
|
+
"- Open pull requests as drafts unless the actor asks for ready review.",
|
|
38
38
|
"",
|
|
39
39
|
"# Slack Output",
|
|
40
40
|
"",
|
|
@@ -555,7 +555,7 @@ export function longReleaseConversation(
|
|
|
555
555
|
outputTokens: 1037,
|
|
556
556
|
},
|
|
557
557
|
surface: "slack",
|
|
558
|
-
|
|
558
|
+
actorIdentity: {
|
|
559
559
|
fullName: "Jordan Blake",
|
|
560
560
|
slackUserId: "UQA777",
|
|
561
561
|
slackUserName: "jordan",
|
|
@@ -585,7 +585,7 @@ export function longReleaseConversation(
|
|
|
585
585
|
outputTokens: 5765,
|
|
586
586
|
},
|
|
587
587
|
surface: "slack",
|
|
588
|
-
|
|
588
|
+
actorIdentity: {
|
|
589
589
|
fullName: "Jordan Blake",
|
|
590
590
|
slackUserId: "UQA777",
|
|
591
591
|
slackUserName: "jordan",
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
ConversationRunReport,
|
|
6
6
|
ConversationSurface,
|
|
7
7
|
ConversationUsage,
|
|
8
|
-
|
|
8
|
+
ActorIdentity,
|
|
9
9
|
TranscriptMessage,
|
|
10
10
|
} from "@sentry/junior/reporting";
|
|
11
11
|
|
|
@@ -24,7 +24,7 @@ export type MockRunOptions = {
|
|
|
24
24
|
id?: string;
|
|
25
25
|
lastProgressAt?: string;
|
|
26
26
|
lastSeenAt?: string;
|
|
27
|
-
|
|
27
|
+
actorIdentity?: ActorIdentity;
|
|
28
28
|
sentryTraceUrl?: string;
|
|
29
29
|
startedAt?: string;
|
|
30
30
|
status?: ConversationReportStatus;
|
|
@@ -64,8 +64,8 @@ export function mockRun(options: MockRunOptions = {}): ConversationRunReport {
|
|
|
64
64
|
...(options.cumulativeUsage !== undefined
|
|
65
65
|
? { cumulativeUsage: options.cumulativeUsage }
|
|
66
66
|
: {}),
|
|
67
|
-
...(options.
|
|
68
|
-
? {
|
|
67
|
+
...(options.actorIdentity !== undefined
|
|
68
|
+
? { actorIdentity: options.actorIdentity }
|
|
69
69
|
: {}),
|
|
70
70
|
...(options.sentryTraceUrl !== undefined
|
|
71
71
|
? { sentryTraceUrl: options.sentryTraceUrl }
|