Skip to content

Commit 3c1598c

Browse files
feat(notifications): inspectable error toasts with details dialog (#3247)
Co-authored-by: Charles Vien <charles.v@posthog.com>
1 parent ca288d4 commit 3c1598c

22 files changed

Lines changed: 717 additions & 75 deletions

packages/ui/src/features/auth/useAuthMutations.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useService } from "@posthog/di/react";
22
import { useHostTRPCClient } from "@posthog/host-router/react";
33
import type { CloudRegion } from "@posthog/shared";
4+
import { clearCapturedLogs } from "@posthog/ui/shell/logCapture";
45
import { useMutation } from "@tanstack/react-query";
56
import { clearAuthScopedQueries, refreshAuthStateQuery } from "./authQueries";
67
import { AUTH_SIDE_EFFECTS, type IAuthSideEffects } from "./identifiers";
@@ -71,6 +72,10 @@ export function useLogoutMutation() {
7172
await hostClient.auth.logout.mutate();
7273
return previous;
7374
},
74-
onSuccess: (previous) => fx.onLogout(previous.cloudRegion),
75+
onSuccess: (previous) => {
76+
// Privacy boundary: error bundles must never export another account's logs.
77+
clearCapturedLogs();
78+
fx.onLogout(previous.cloudRegion);
79+
},
7580
});
7681
}

packages/ui/src/features/canvas/hooks/useGenerateContext.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import type { WorkspaceMode } from "@posthog/shared";
88
import { buildContextGenerationPrompt } from "@posthog/ui/features/canvas/contextPrompt";
99
import { useChannelTaskMutations } from "@posthog/ui/features/canvas/hooks/useChannelTasks";
1010
import { useFolderGenerationTaskMutation } from "@posthog/ui/features/canvas/hooks/useFolderGenerationTask";
11+
import { toastError } from "@posthog/ui/features/notifications/errorDetails";
1112
import { useCreateTask } from "@posthog/ui/features/tasks/useTaskCrudMutations";
12-
import { toast } from "@posthog/ui/primitives/toast";
1313
import { useQueryClient } from "@tanstack/react-query";
1414
import { useCallback, useState } from "react";
1515

@@ -47,9 +47,7 @@ export function useGenerateContext(channelId: string, channelName: string) {
4747
);
4848

4949
if (!result.success) {
50-
toast.error("Couldn't start CONTEXT.md generation", {
51-
description: result.error,
52-
});
50+
toastError("Couldn't start CONTEXT.md generation", result.error);
5351
return null;
5452
}
5553

packages/ui/src/features/canvas/hooks/useGenerateFreeformCanvas.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from "@posthog/ui/features/canvas/hooks/useDashboards";
2121
import { useFolderInstructions } from "@posthog/ui/features/canvas/hooks/useFolderInstructions";
2222
import { useCanvasGenerationTrackerStore } from "@posthog/ui/features/canvas/stores/canvasGenerationTrackerStore";
23+
import { toastError } from "@posthog/ui/features/notifications/errorDetails";
2324
import { useCreateTask } from "@posthog/ui/features/tasks/useTaskCrudMutations";
2425
import { toast } from "@posthog/ui/primitives/toast";
2526
import { useQueryClient } from "@tanstack/react-query";
@@ -123,9 +124,7 @@ export function useGenerateFreeformCanvas(args: {
123124
);
124125

125126
if (!result.success) {
126-
toast.error("Couldn't start canvas generation", {
127-
description: result.error,
128-
});
127+
toastError("Couldn't start canvas generation", result.error);
129128
return null;
130129
}
131130

packages/ui/src/features/external-apps/useExternalAppAction.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useService } from "@posthog/di/react";
88
import { useCallback } from "react";
99
import { toast } from "../../primitives/toast";
1010
import { showFocusSuccessToast } from "../focus/focusToast";
11+
import { toastError } from "../notifications/errorDetails";
1112

1213
export function useExternalAppAction() {
1314
const service = useService<ExternalAppService>(EXTERNAL_APPS_SERVICE);
@@ -39,14 +40,10 @@ export function useExternalAppAction() {
3940
});
4041
return;
4142
case "open-failed":
42-
toast.error("Failed to open in external app", {
43-
description: outcome.error,
44-
});
43+
toastError("Failed to open in external app", outcome.error);
4544
return;
4645
case "focus-failed":
47-
toast.error("Could not edit workspace", {
48-
description: outcome.error,
49-
});
46+
toastError("Could not edit workspace", outcome.error);
5047
return;
5148
case "copied":
5249
toast.success("Path copied to clipboard", {

packages/ui/src/features/focus/focus-events.contribution.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import {
44
type HostTrpcClient,
55
} from "@posthog/host-router/client";
66
import { inject, injectable } from "inversify";
7-
import { toast } from "../../primitives/toast";
87
import { logger } from "../../shell/logger";
98
import {
109
IMPERATIVE_QUERY_CLIENT,
1110
type ImperativeQueryClient,
1211
} from "../../shell/queryClient";
12+
import { toastError } from "../notifications/errorDetails";
1313
import { WORKSPACE_QUERY_KEY } from "../workspace/identifiers";
1414
import { useFocusStore } from "./focusStore";
1515

@@ -47,9 +47,7 @@ export class FocusEventsContribution implements Contribution {
4747
);
4848
const result = await useFocusStore.getState().disableFocus();
4949
if (!result.success && result.error) {
50-
toast.error("Could not unfocus workspace", {
51-
description: result.error,
52-
});
50+
toastError("Could not unfocus workspace", result.error);
5351
}
5452
},
5553
});

packages/ui/src/features/home/hooks/useRunWorkstreamAction.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { homeKeys } from "@posthog/ui/features/home/hooks/useHomeSnapshot";
1717
import { useQuickActionStore } from "@posthog/ui/features/home/stores/quickActionStore";
1818
import { insertOptimisticTask } from "@posthog/ui/features/home/utils/optimisticTask";
1919
import { useUserRepositoryIntegration } from "@posthog/ui/features/integrations/useIntegrations";
20+
import { toastError } from "@posthog/ui/features/notifications/errorDetails";
2021
import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore";
2122
import { useCreateTask } from "@posthog/ui/features/tasks/useTaskCrudMutations";
2223
import { useAuthenticatedMutation } from "@posthog/ui/hooks/useAuthenticatedMutation";
@@ -170,16 +171,14 @@ export function useRunWorkstreamAction(): RunWorkstreamAction {
170171
});
171172
return;
172173
}
173-
toast.error("Failed to start task", { description: result.error });
174+
toastError("Failed to start task", result.error);
174175
log.error("Quick action task creation failed", {
175176
failedStep: result.failedStep,
176177
error: result.error,
177178
});
178179
fallbackToTaskInput();
179180
} catch (error) {
180-
const description =
181-
error instanceof Error ? error.message : "Unknown error";
182-
toast.error("Failed to start task", { description });
181+
toastError("Failed to start task", error);
183182
log.error("Quick action task creation threw", { error });
184183
fallbackToTaskInput();
185184
} finally {

packages/ui/src/features/inbox/components/ConfigureAgentsSection.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
useRepositoryIntegration,
3333
useUserRepositoryIntegration,
3434
} from "@posthog/ui/features/integrations/useIntegrations";
35+
import { toastError } from "@posthog/ui/features/notifications/errorDetails";
3536
import { ScoutsFleetSection } from "@posthog/ui/features/scouts/components/ScoutsFleetSection";
3637
import { GitHubIntegrationSection } from "@posthog/ui/features/settings/sections/GitHubIntegrationSection";
3738
import { SlackInboxNotificationsSettings } from "@posthog/ui/features/settings/sections/SlackInboxNotificationsSettings";
@@ -365,9 +366,7 @@ function SetupTaskSection() {
365366
adapter,
366367
});
367368
} else {
368-
toast.error("Failed to start Self-driving setup", {
369-
description: result.error,
370-
});
369+
toastError("Failed to start Self-driving setup", result.error);
371370
log.error("Self-driving setup task creation failed", {
372371
failedStep: result.failedStep,
373372
error: result.error,
@@ -380,9 +379,7 @@ function SetupTaskSection() {
380379
action_type: "run_setup_agent",
381380
success: false,
382381
});
383-
const description =
384-
error instanceof Error ? error.message : "Unknown error";
385-
toast.error("Failed to start Self-driving setup", { description });
382+
toastError("Failed to start Self-driving setup", error);
386383
log.error("Unexpected error during Self-driving setup task creation", {
387384
error,
388385
repository: setupRepository,

packages/ui/src/features/inbox/hooks/useInboxCloudTaskRunner.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { useAuthStateValue } from "@posthog/ui/features/auth/store";
1919
import { showOfflineToast } from "@posthog/ui/features/connectivity/connectivityToast";
2020
import { resolveDefaultModel } from "@posthog/ui/features/inbox/hooks/resolveDefaultModel";
2121
import { useUserRepositoryIntegration } from "@posthog/ui/features/integrations/useIntegrations";
22+
import { toastError } from "@posthog/ui/features/notifications/errorDetails";
2223
import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore";
2324
import { useCreateTask } from "@posthog/ui/features/tasks/useTaskCrudMutations";
2425
import { useConnectivity } from "@posthog/ui/hooks/useConnectivity";
@@ -262,7 +263,7 @@ export function useInboxCloudTaskRunner({
262263
toast.dismiss(toastId);
263264
// Usage-limit blocks already show the upgrade modal; don't double-toast.
264265
if (!isUsageLimitResult(result)) {
265-
toast.error(copy.errorTitle, { description: result.error });
266+
toastError(copy.errorTitle, result.error);
266267
log.error("Cloud-task creation failed", {
267268
failedStep: result.failedStep,
268269
error: result.error,
@@ -273,9 +274,7 @@ export function useInboxCloudTaskRunner({
273274
}
274275
} catch (error) {
275276
toast.dismiss(toastId);
276-
const description =
277-
error instanceof Error ? error.message : "Unknown error";
278-
toast.error(copy.errorTitle, { description });
277+
toastError(copy.errorTitle, error);
279278
log.error("Unexpected error during cloud-task creation", {
280279
error,
281280
reportId,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
Badge,
3+
Button,
4+
Dialog,
5+
DialogContent,
6+
DialogFooter,
7+
DialogHeader,
8+
DialogTitle,
9+
} from "@posthog/quill";
10+
import { CodeBlock } from "@posthog/ui/primitives/CodeBlock";
11+
import { openTaskInput } from "@posthog/ui/router/useOpenTask";
12+
import { formatCapturedLogs } from "@posthog/ui/shell/logCapture";
13+
import { serializeError, useErrorDetailsStore } from "./errorDetails";
14+
15+
// Keep the create-task prompt readable: the full 500-entry buffer belongs in
16+
// the downloaded bundle, not in a composer draft.
17+
const TASK_PROMPT_LOG_ENTRIES = 100;
18+
19+
function buildBundle(
20+
title: string,
21+
occurredAt: number,
22+
prettyError: string,
23+
): string {
24+
return [
25+
`# ${title}`,
26+
`Occurred at: ${new Date(occurredAt).toISOString()}`,
27+
"",
28+
"## Error",
29+
prettyError,
30+
"",
31+
"## Recent logs",
32+
formatCapturedLogs(),
33+
"",
34+
].join("\n");
35+
}
36+
37+
// Global inspector behind every error toast's "Details" action: the full
38+
// pretty-printed payload the toast had no room for, a downloadable
39+
// error-plus-logs bundle, and (dev builds only) a one-click task prefilled
40+
// with the same context.
41+
export function ErrorDetailsDialog() {
42+
const detail = useErrorDetailsStore((s) => s.detail);
43+
const close = useErrorDetailsStore((s) => s.close);
44+
if (!detail) return null;
45+
46+
const prettyError = serializeError(detail.error);
47+
48+
const handleDownload = () => {
49+
const bundle = buildBundle(detail.title, detail.occurredAt, prettyError);
50+
const blob = new Blob([bundle], { type: "text/markdown" });
51+
const url = URL.createObjectURL(blob);
52+
const anchor = document.createElement("a");
53+
anchor.href = url;
54+
anchor.download = `posthog-code-error-${detail.occurredAt}.md`;
55+
anchor.click();
56+
URL.revokeObjectURL(url);
57+
};
58+
59+
const handleCreateTask = () => {
60+
close();
61+
openTaskInput({
62+
initialPrompt: [
63+
`Investigate this error from the PostHog Code app: ${detail.title}`,
64+
"",
65+
"## Error",
66+
"```",
67+
prettyError,
68+
"```",
69+
"",
70+
"## Recent logs",
71+
"```",
72+
formatCapturedLogs({ maxEntries: TASK_PROMPT_LOG_ENTRIES }),
73+
"```",
74+
].join("\n"),
75+
});
76+
};
77+
78+
return (
79+
<Dialog open onOpenChange={(open) => !open && close()}>
80+
<DialogContent className="w-[min(720px,calc(100vw-32px))] max-w-[720px] sm:max-w-[720px]">
81+
<DialogHeader>
82+
<DialogTitle>{detail.title}</DialogTitle>
83+
</DialogHeader>
84+
<div className="max-h-[55vh] overflow-y-auto px-1">
85+
<CodeBlock size="1">{prettyError}</CodeBlock>
86+
</div>
87+
<DialogFooter>
88+
{import.meta.env.DEV && (
89+
<Button
90+
variant="outline"
91+
size="sm"
92+
onClick={handleCreateTask}
93+
className="gap-2 sm:mr-auto"
94+
>
95+
Create task from error
96+
<Badge variant="warning">Dev</Badge>
97+
</Button>
98+
)}
99+
<Button variant="outline" size="sm" onClick={handleDownload}>
100+
Download error + logs
101+
</Button>
102+
<Button variant="primary" size="sm" onClick={close}>
103+
Close
104+
</Button>
105+
</DialogFooter>
106+
</DialogContent>
107+
</Dialog>
108+
);
109+
}

0 commit comments

Comments
 (0)