Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1645,4 +1645,43 @@ export class PostHogAPIClient {
);
}
}

/** Find an exported asset by session recording ID. */
async findExportBySessionRecordingId(
projectId: number,
sessionRecordingId: string,
): Promise<number | null> {
const urlPath = `/api/projects/${projectId}/exports/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
url.searchParams.set("session_recording_id", sessionRecordingId);
url.searchParams.set("export_format", "video/mp4");
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const data = (await response.json()) as {
results?: Array<{ id: number; has_content: boolean }>;
};
const match = data.results?.find((e) => e.has_content);
return match?.id ?? null;
}

/** Get the presigned content URL for an exported asset (e.g. rasterized recording). */
async getExportContentUrl(
projectId: number,
exportId: number,
): Promise<string | null> {
const urlPath = `/api/projects/${projectId}/exports/${exportId}/content/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "get",
url,
path: urlPath,
});
if (!response.ok) return null;
const blob = await response.blob();
return URL.createObjectURL(blob);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
ArrowSquareOutIcon,
CaretDownIcon,
CaretRightIcon,
ClockIcon,
Cloud as CloudIcon,
CommandIcon,
GithubLogoIcon,
Expand All @@ -38,8 +37,6 @@ import type {
PriorityJudgmentArtefact,
SignalFindingArtefact,
SignalReport,
SignalReportArtefact,
SignalReportArtefactsResponse,
SuggestedReviewersArtefact,
} from "@shared/types";
import { useNavigationStore } from "@stores/navigationStore";
Expand All @@ -60,23 +57,6 @@ import { SignalCard } from "./SignalCard";

// ── Helpers ─────────────────────────────────────────────────────────────────

function getArtefactsUnavailableMessage(
reason: SignalReportArtefactsResponse["unavailableReason"],
): string {
switch (reason) {
case "forbidden":
return "Evidence could not be loaded with the current API permissions.";
case "not_found":
return "Evidence endpoint is unavailable for this signal in this environment.";
case "invalid_payload":
return "Evidence format was unexpected, so no artefacts could be shown.";
case "request_failed":
return "Evidence is temporarily unavailable. You can still create a task from this report.";
default:
return "Evidence is currently unavailable for this signal.";
}
}

function DetailRow({
label,
value,
Expand Down Expand Up @@ -151,10 +131,6 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
});
const allArtefacts = artefactsQuery.data?.results ?? [];

const videoSegments = allArtefacts.filter(
(a): a is SignalReportArtefact => a.type === "video_segment",
);

const suggestedReviewers = useMemo(() => {
const reviewerArtefact = allArtefacts.find(
(a): a is SuggestedReviewersArtefact => a.type === "suggested_reviewers",
Expand Down Expand Up @@ -193,17 +169,24 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
}, [allArtefacts]);

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

const signalsQuery = useInboxReportSignals(report.id, {
enabled: true,
});
const signals = signalsQuery.data?.signals ?? [];
const allSignals = signalsQuery.data?.signals ?? [];
const sessionProblemSignals = allSignals.filter(
(s) =>
s.source_product === "session_replay" &&
s.source_type === "session_problem",
);
const signals = allSignals.filter(
(s) =>
!(
s.source_product === "session_replay" &&
s.source_type === "session_problem"
),
);

// ── Task creation ───────────────────────────────────────────────────────
const { navigateToTaskInput, navigateToTask } = useNavigationStore();
Expand All @@ -225,11 +208,11 @@ export function ReportDetailPane({ report, onClose }: ReportDetailPaneProps) {
const buildPrompt = useCallback(() => {
return buildSignalTaskPrompt({
report,
artefacts: videoSegments,
signals,
artefacts: [],
signals: [...signals, ...sessionProblemSignals],
replayBaseUrl,
});
}, [report, videoSegments, signals, replayBaseUrl]);
}, [report, signals, sessionProblemSignals, replayBaseUrl]);

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

{/* ── Evidence (session segments) ─────────────────────── */}
<Box>
<Text size="1" weight="medium" className="block text-[13px]" mb="2">
Evidence
</Text>
{artefactsQuery.isLoading && (
<Text size="1" color="gray" className="block text-[12px]">
Loading evidence...
</Text>
)}
{showArtefactsUnavailable && (
<Text size="1" color="gray" className="block text-[12px]">
{artefactsUnavailableMessage}
{/* ── Session problem evidence ─────────────────────────── */}
{sessionProblemSignals.length > 0 && (
<Box>
<Text
size="1"
weight="medium"
className="block text-[13px]"
mb="2"
>
Evidence ({sessionProblemSignals.length})
</Text>
)}
{!artefactsQuery.isLoading &&
!showArtefactsUnavailable &&
videoSegments.length === 0 && (
<Text size="1" color="gray" className="block text-[12px]">
No session segments available for this report.
</Text>
)}
<Flex direction="column" gap="1">
{videoSegments.map((artefact) => (
<Box
key={artefact.id}
className="rounded border border-gray-6 bg-gray-1 p-2"
>
<Text
size="1"
className="whitespace-pre-wrap text-pretty break-words text-[12px]"
>
{artefact.content.content}
</Text>
<Flex align="center" justify="between" mt="1" gap="2">
<Flex align="center" gap="1">
<ClockIcon size={12} className="text-gray-9" />
<Text size="1" color="gray" className="text-[12px]">
{artefact.content.start_time
? new Date(
artefact.content.start_time,
).toLocaleString()
: "Unknown time"}
</Text>
</Flex>
{replayBaseUrl && artefact.content.session_id && (
<a
href={`${replayBaseUrl}/${artefact.content.session_id}`}
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-[12px] text-gray-11 hover:text-gray-12"
>
View replay
<ArrowSquareOutIcon size={12} />
</a>
)}
</Flex>
</Box>
))}
</Flex>
</Box>
<Flex direction="column" gap="2">
{sessionProblemSignals.map((signal) => (
<SignalCard
key={signal.signal_id}
signal={signal}
finding={signalFindings.get(signal.signal_id)}
/>
))}
</Flex>
</Box>
)}
</Flex>
</ScrollArea>

Expand Down
Loading
Loading