Skip to content

Commit 35fbba7

Browse files
committed
feat(inbox): support session problem signals
Add dedicated rendering for session_problem signals in the inbox detail pane, including video playback and an "Events around the problem" table showing user-behavior events with human-readable names and millisecond timestamps.
1 parent d95d46a commit 35fbba7

File tree

3 files changed

+345
-95
lines changed

3 files changed

+345
-95
lines changed

apps/code/src/renderer/api/posthogClient.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,4 +1645,43 @@ export class PostHogAPIClient {
16451645
);
16461646
}
16471647
}
1648+
1649+
/** Find an exported asset by session recording ID. */
1650+
async findExportBySessionRecordingId(
1651+
projectId: number,
1652+
sessionRecordingId: string,
1653+
): Promise<number | null> {
1654+
const urlPath = `/api/projects/${projectId}/exports/`;
1655+
const url = new URL(`${this.api.baseUrl}${urlPath}`);
1656+
url.searchParams.set("session_recording_id", sessionRecordingId);
1657+
url.searchParams.set("export_format", "video/mp4");
1658+
const response = await this.api.fetcher.fetch({
1659+
method: "get",
1660+
url,
1661+
path: urlPath,
1662+
});
1663+
if (!response.ok) return null;
1664+
const data = (await response.json()) as {
1665+
results?: Array<{ id: number; has_content: boolean }>;
1666+
};
1667+
const match = data.results?.find((e) => e.has_content);
1668+
return match?.id ?? null;
1669+
}
1670+
1671+
/** Get the presigned content URL for an exported asset (e.g. rasterized recording). */
1672+
async getExportContentUrl(
1673+
projectId: number,
1674+
exportId: number,
1675+
): Promise<string | null> {
1676+
const urlPath = `/api/projects/${projectId}/exports/${exportId}/content/`;
1677+
const url = new URL(`${this.api.baseUrl}${urlPath}`);
1678+
const response = await this.api.fetcher.fetch({
1679+
method: "get",
1680+
url,
1681+
path: urlPath,
1682+
});
1683+
if (!response.ok) return null;
1684+
const blob = await response.blob();
1685+
return URL.createObjectURL(blob);
1686+
}
16481687
}

apps/code/src/renderer/features/inbox/components/detail/ReportDetailPane.tsx

Lines changed: 38 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
ArrowSquareOutIcon,
1414
CaretDownIcon,
1515
CaretRightIcon,
16-
ClockIcon,
1716
Cloud as CloudIcon,
1817
CommandIcon,
1918
GithubLogoIcon,
@@ -38,8 +37,6 @@ import type {
3837
PriorityJudgmentArtefact,
3938
SignalFindingArtefact,
4039
SignalReport,
41-
SignalReportArtefact,
42-
SignalReportArtefactsResponse,
4340
SuggestedReviewersArtefact,
4441
} from "@shared/types";
4542
import { useNavigationStore } from "@stores/navigationStore";
@@ -60,23 +57,6 @@ import { SignalCard } from "./SignalCard";
6057

6158
// ── Helpers ─────────────────────────────────────────────────────────────────
6259

63-
function getArtefactsUnavailableMessage(
64-
reason: SignalReportArtefactsResponse["unavailableReason"],
65-
): string {
66-
switch (reason) {
67-
case "forbidden":
68-
return "Evidence could not be loaded with the current API permissions.";
69-
case "not_found":
70-
return "Evidence endpoint is unavailable for this signal in this environment.";
71-
case "invalid_payload":
72-
return "Evidence format was unexpected, so no artefacts could be shown.";
73-
case "request_failed":
74-
return "Evidence is temporarily unavailable. You can still create a task from this report.";
75-
default:
76-
return "Evidence is currently unavailable for this signal.";
77-
}
78-
}
79-
8060
function DetailRow({
8161
label,
8262
value,
@@ -151,10 +131,6 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
151131
});
152132
const allArtefacts = artefactsQuery.data?.results ?? [];
153133

154-
const videoSegments = allArtefacts.filter(
155-
(a): a is SignalReportArtefact => a.type === "video_segment",
156-
);
157-
158134
const suggestedReviewers = useMemo(() => {
159135
const reviewerArtefact = allArtefacts.find(
160136
(a): a is SuggestedReviewersArtefact => a.type === "suggested_reviewers",
@@ -193,17 +169,24 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
193169
}, [allArtefacts]);
194170

195171
const artefactsUnavailableReason = artefactsQuery.data?.unavailableReason;
196-
const showArtefactsUnavailable =
197-
!artefactsQuery.isLoading &&
198-
(!!artefactsQuery.error || !!artefactsUnavailableReason);
199-
const artefactsUnavailableMessage = artefactsQuery.error
200-
? "Evidence could not be loaded right now. You can still create a task from this report."
201-
: getArtefactsUnavailableMessage(artefactsUnavailableReason);
172+
void artefactsUnavailableReason; // TODO: wire up unavailable UI
202173

203174
const signalsQuery = useInboxReportSignals(report.id, {
204175
enabled: true,
205176
});
206-
const signals = signalsQuery.data?.signals ?? [];
177+
const allSignals = signalsQuery.data?.signals ?? [];
178+
const sessionProblemSignals = allSignals.filter(
179+
(s) =>
180+
s.source_product === "session_replay" &&
181+
s.source_type === "session_problem",
182+
);
183+
const signals = allSignals.filter(
184+
(s) =>
185+
!(
186+
s.source_product === "session_replay" &&
187+
s.source_type === "session_problem"
188+
),
189+
);
207190

208191
// ── Task creation ───────────────────────────────────────────────────────
209192
const { navigateToTaskInput, navigateToTask } = useNavigationStore();
@@ -225,11 +208,11 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
225208
const buildPrompt = useCallback(() => {
226209
return buildSignalTaskPrompt({
227210
report,
228-
artefacts: videoSegments,
229-
signals,
211+
artefacts: [],
212+
signals: [...signals, ...sessionProblemSignals],
230213
replayBaseUrl,
231214
});
232-
}, [report, videoSegments, signals, replayBaseUrl]);
215+
}, [report, signals, sessionProblemSignals, replayBaseUrl]);
233216

234217
const handleCreateTask = useCallback(() => {
235218
if (!canActOnReport) return;
@@ -527,67 +510,28 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
527510
</Text>
528511
)}
529512

530-
{/* ── Evidence (session segments) ─────────────────────── */}
531-
<Box>
532-
<Text size="1" weight="medium" className="block text-[13px]" mb="2">
533-
Evidence
534-
</Text>
535-
{artefactsQuery.isLoading && (
536-
<Text size="1" color="gray" className="block text-[12px]">
537-
Loading evidence...
538-
</Text>
539-
)}
540-
{showArtefactsUnavailable && (
541-
<Text size="1" color="gray" className="block text-[12px]">
542-
{artefactsUnavailableMessage}
513+
{/* ── Session problem evidence ─────────────────────────── */}
514+
{sessionProblemSignals.length > 0 && (
515+
<Box>
516+
<Text
517+
size="1"
518+
weight="medium"
519+
className="block text-[13px]"
520+
mb="2"
521+
>
522+
Evidence ({sessionProblemSignals.length})
543523
</Text>
544-
)}
545-
{!artefactsQuery.isLoading &&
546-
!showArtefactsUnavailable &&
547-
videoSegments.length === 0 && (
548-
<Text size="1" color="gray" className="block text-[12px]">
549-
No session segments available for this report.
550-
</Text>
551-
)}
552-
<Flex direction="column" gap="1">
553-
{videoSegments.map((artefact) => (
554-
<Box
555-
key={artefact.id}
556-
className="rounded border border-gray-6 bg-gray-1 p-2"
557-
>
558-
<Text
559-
size="1"
560-
className="whitespace-pre-wrap text-pretty break-words text-[12px]"
561-
>
562-
{artefact.content.content}
563-
</Text>
564-
<Flex align="center" justify="between" mt="1" gap="2">
565-
<Flex align="center" gap="1">
566-
<ClockIcon size={12} className="text-gray-9" />
567-
<Text size="1" color="gray" className="text-[12px]">
568-
{artefact.content.start_time
569-
? new Date(
570-
artefact.content.start_time,
571-
).toLocaleString()
572-
: "Unknown time"}
573-
</Text>
574-
</Flex>
575-
{replayBaseUrl && artefact.content.session_id && (
576-
<a
577-
href={`${replayBaseUrl}/${artefact.content.session_id}`}
578-
target="_blank"
579-
rel="noreferrer"
580-
className="inline-flex items-center gap-1 text-[12px] text-gray-11 hover:text-gray-12"
581-
>
582-
View replay
583-
<ArrowSquareOutIcon size={12} />
584-
</a>
585-
)}
586-
</Flex>
587-
</Box>
588-
))}
589-
</Flex>
590-
</Box>
524+
<Flex direction="column" gap="2">
525+
{sessionProblemSignals.map((signal) => (
526+
<SignalCard
527+
key={signal.signal_id}
528+
signal={signal}
529+
finding={signalFindings.get(signal.signal_id)}
530+
/>
531+
))}
532+
</Flex>
533+
</Box>
534+
)}
591535
</Flex>
592536
</ScrollArea>
593537

0 commit comments

Comments
 (0)