Skip to content

Commit 43e61aa

Browse files
dcramercodex
andauthored
feat(identity): Add SQL-backed people API (#738)
People list and profile now read from a durable SQL identity model instead of JuniorReporting or turn-session summaries. Conversation writes upsert verified provider identities into `junior_users` by normalized email, so trusted Slack requester observations share one user identity case-insensitively. The dashboard mounts per-endpoint people API wrappers over `@sentry/junior/api/people/list` and `/profile`, while plugin reporting remains the extension point for plugin APIs. The public people subpaths expose only the read APIs; source-local query helpers keep integration tests on real Drizzle fixtures. Review order: start with the identity schema/migration and `upsertIdentity`, then the people API endpoints and dashboard route wrappers, then the reporting/test deletions. --------- Co-authored-by: GPT-5 Codex <codex@openai.com>
1 parent 5302930 commit 43e61aa

42 files changed

Lines changed: 2189 additions & 1402 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { readPeopleList } from "@sentry/junior/api/people/list";
2+
3+
/** Serve the dashboard people list endpoint from the SQL-backed people API. */
4+
export async function peopleListResponse(): Promise<Response> {
5+
try {
6+
return Response.json(await readPeopleList());
7+
} catch (error) {
8+
console.error("Failed to load people list", error);
9+
return Response.json(
10+
{ error: "Failed to load people list" },
11+
{ status: 500 },
12+
);
13+
}
14+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { readPeopleProfile } from "@sentry/junior/api/people/profile";
2+
3+
/** Serve the dashboard person profile endpoint from the SQL-backed people API. */
4+
export async function peopleProfileResponse(email: string): Promise<Response> {
5+
if (!email.trim()) {
6+
return Response.json({ error: "Email is required" }, { status: 400 });
7+
}
8+
9+
try {
10+
return Response.json(await readPeopleProfile(email));
11+
} catch (error) {
12+
console.error("Failed to load person profile", error);
13+
return Response.json(
14+
{ error: "Failed to load person profile" },
15+
{ status: 500 },
16+
);
17+
}
18+
}

packages/junior-dashboard/src/app.ts

Lines changed: 4 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import type {
55
ConversationStatsReport,
66
PluginOperationalReportFeed,
77
JuniorReporting,
8-
RequesterDirectoryReport,
9-
RequesterProfileReport,
108
} from "@sentry/junior/reporting";
119
import { createJuniorReporting } from "@sentry/junior/reporting";
1210
import { initSentry } from "@sentry/junior/instrumentation";
@@ -25,6 +23,8 @@ import {
2523
} from "./auth";
2624
import { dashboardRainbowProgressClass } from "./dashboardLoader";
2725
import { createMockConversationReporting } from "./mock-conversations";
26+
import { peopleListResponse } from "./api/people/list";
27+
import { peopleProfileResponse } from "./api/people/profile";
2828

2929
const DEFAULT_BASE_PATH = "/";
3030
const DEFAULT_AUTH_PATH = "/api/auth";
@@ -235,48 +235,6 @@ function emptyConversationStatsReport(): ConversationStatsReport {
235235
};
236236
}
237237

238-
function emptyRequesterDirectoryReport(): RequesterDirectoryReport {
239-
return {
240-
generatedAt: new Date().toISOString(),
241-
people: [],
242-
sampleLimit: 0,
243-
sampleSize: 0,
244-
source: "conversation_index",
245-
truncated: false,
246-
};
247-
}
248-
249-
function emptyRequesterProfileReport(email: string): RequesterProfileReport {
250-
const nowMs = Date.now();
251-
const end = new Date(nowMs);
252-
end.setUTCHours(0, 0, 0, 0);
253-
const start = new Date(end);
254-
start.setUTCDate(start.getUTCDate() - 365);
255-
return {
256-
activityDays: [],
257-
generatedAt: new Date(nowMs).toISOString(),
258-
locations: [],
259-
recentConversations: [],
260-
requester: { email },
261-
sampleLimit: 0,
262-
sampleSize: 0,
263-
source: "conversation_index",
264-
surfaces: [],
265-
totals: {
266-
active: 0,
267-
activeDays: 0,
268-
conversations: 0,
269-
durationMs: 0,
270-
failed: 0,
271-
hung: 0,
272-
runs: 0,
273-
},
274-
truncated: false,
275-
windowEnd: end.toISOString(),
276-
windowStart: start.toISOString(),
277-
};
278-
}
279-
280238
async function readConversationStats(
281239
reporting: JuniorReporting,
282240
): Promise<ConversationStatsReport> {
@@ -295,25 +253,6 @@ async function readPluginReports(
295253
return await reporting.getPluginOperationalReports();
296254
}
297255

298-
async function readRequesterDirectory(
299-
reporting: JuniorReporting,
300-
): Promise<RequesterDirectoryReport> {
301-
if (!reporting.listRequesters) {
302-
return emptyRequesterDirectoryReport();
303-
}
304-
return await reporting.listRequesters();
305-
}
306-
307-
async function readRequesterProfile(
308-
reporting: JuniorReporting,
309-
email: string,
310-
): Promise<RequesterProfileReport> {
311-
if (!reporting.getRequesterProfile) {
312-
return emptyRequesterProfileReport(email);
313-
}
314-
return await reporting.getRequesterProfile(email);
315-
}
316-
317256
function callbackUrl(request: Request, basePath: string): string {
318257
const requestUrl = new URL(request.url);
319258
const returnPath = requestedReturnPath(requestUrl, basePath);
@@ -712,30 +651,9 @@ export function createDashboardApp(
712651
);
713652
}
714653
});
715-
app.get("/api/people", async () => {
716-
try {
717-
return Response.json(await readRequesterDirectory(reporting));
718-
} catch {
719-
return Response.json(
720-
{ error: "People failed to load." },
721-
{ status: 500 },
722-
);
723-
}
724-
});
654+
app.get("/api/people", () => peopleListResponse());
725655
app.get("/api/people/:email", async (c) => {
726-
try {
727-
return Response.json(
728-
await readRequesterProfile(
729-
reporting,
730-
decodeURIComponent(c.req.param("email")),
731-
),
732-
);
733-
} catch {
734-
return Response.json(
735-
{ error: "Profile failed to load." },
736-
{ status: 500 },
737-
);
738-
}
656+
return peopleProfileResponse(decodeURIComponent(c.req.param("email")));
739657
});
740658
app.get("/api/plugin-reports", async () => {
741659
try {

packages/junior-dashboard/src/client/types.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@ import type {
66
ConversationSubagentTranscriptReport as ReportingConversationSubagentTranscriptReport,
77
PluginOperationalReportFeed,
88
PluginOperationalReport,
9-
RequesterActivityDayReport,
10-
RequesterDirectoryReport,
11-
RequesterIdentity as ReportingRequesterIdentity,
12-
RequesterProfileReport,
13-
RequesterSummaryReport,
14-
RequesterTotalsReport,
159
ConversationFeed as ReportingConversationFeed,
1610
ConversationSummaryReport,
1711
ConversationRunReport,
@@ -21,6 +15,16 @@ import type {
2115
RuntimeInfoReport,
2216
SkillReport,
2317
} from "@sentry/junior/reporting";
18+
import type {
19+
RequesterDirectoryReport,
20+
RequesterIdentity as ApiRequesterIdentity,
21+
RequesterSummaryReport,
22+
RequesterTotalsReport,
23+
} from "@sentry/junior/api/people/list";
24+
import type {
25+
RequesterActivityDayReport,
26+
RequesterProfileReport,
27+
} from "@sentry/junior/api/people/profile";
2428

2529
export type Health = HealthReport;
2630

@@ -41,7 +45,7 @@ export type ConversationSubagentTranscript =
4145

4246
export type ConversationStatsItem = ReportingConversationStatsItem;
4347

44-
export type RequesterIdentity = ReportingRequesterIdentity;
48+
export type RequesterIdentity = ApiRequesterIdentity;
4549

4650
export type RequesterActivityDay = RequesterActivityDayReport;
4751

0 commit comments

Comments
 (0)