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
2 changes: 1 addition & 1 deletion apps/code/src/main/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export function createWindow(): void {
...platformWindowConfig,
show: false,
webPreferences: {
nodeIntegration: true,
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, "preload.js"),
enableBlinkFeatures: "GetDisplayMedia",
Expand Down
95 changes: 95 additions & 0 deletions apps/code/src/renderer/components/BootErrorBoundary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";

const { logError } = vi.hoisted(() => ({ logError: vi.fn() }));

vi.mock("@posthog/ui/shell/logger", () => ({
logger: {
scope: () => ({
error: logError,
info: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}),
},
}));

import {
BootErrorBoundary,
BootErrorScreen,
} from "@components/BootErrorBoundary";

function ThrowOnRender({ message }: { message: string }): never {
throw new Error(message);
}

describe("BootErrorScreen", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});

it("renders the alert, title and an Error message", () => {
render(<BootErrorScreen error={new Error("kaboom")} />);

expect(screen.getByRole("alert")).toBeInTheDocument();
expect(
screen.getByText("PostHog Code failed to start"),
).toBeInTheDocument();
expect(screen.getByText("kaboom")).toBeInTheDocument();
});

it("stringifies a non-Error value", () => {
render(<BootErrorScreen error="plain string failure" />);

expect(screen.getByText("plain string failure")).toBeInTheDocument();
});

it("reloads the window when Reload is clicked", () => {
const reload = vi.fn();
vi.stubGlobal("location", { reload });

render(<BootErrorScreen error={new Error("x")} />);
fireEvent.click(screen.getByRole("button", { name: /reload/i }));

expect(reload).toHaveBeenCalledTimes(1);
});
});

describe("BootErrorBoundary", () => {
afterEach(() => {
vi.restoreAllMocks();
});

it("renders children when no error is thrown", () => {
render(
<BootErrorBoundary>
<span>healthy child</span>
</BootErrorBoundary>,
);

expect(screen.getByText("healthy child")).toBeInTheDocument();
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
});

it("catches a render error, shows the fallback and logs it", () => {
vi.spyOn(console, "error").mockImplementation(() => {});

render(
<BootErrorBoundary>
<ThrowOnRender message="render boom" />
</BootErrorBoundary>,
);

expect(screen.getByRole("alert")).toBeInTheDocument();
expect(screen.getByText("render boom")).toBeInTheDocument();
expect(logError).toHaveBeenCalled();
});

it("derives error state from a thrown error", () => {
const error = new Error("derive");
expect(BootErrorBoundary.getDerivedStateFromError(error)).toEqual({
error,
});
});
});
100 changes: 100 additions & 0 deletions apps/code/src/renderer/components/BootErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { logger } from "@posthog/ui/shell/logger";
import React, { useEffect, useRef } from "react";

const log = logger.scope("boot-error");

// Inline styles intentionally: this screen must render even when the app's
// theme, CSS or design-system providers failed to load during a boot failure.
const screenStyle: React.CSSProperties = {
position: "fixed",
inset: 0,
outline: "none",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: 16,
padding: 24,
backgroundColor: "#0a0a0a",
color: "#fafafa",
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
textAlign: "center",
};

const titleStyle: React.CSSProperties = {
margin: 0,
fontSize: 18,
fontWeight: 600,
};

const messageStyle: React.CSSProperties = {
margin: 0,
maxWidth: 480,
fontSize: 13,
opacity: 0.8,
};

const buttonStyle: React.CSSProperties = {
padding: "6px 16px",
fontSize: 13,
fontWeight: 500,
color: "#0a0a0a",
backgroundColor: "#fafafa",
border: "none",
borderRadius: 6,
cursor: "pointer",
};

export function BootErrorScreen({ error }: { error: unknown }) {
const containerRef = useRef<HTMLDivElement>(null);
// Move focus to the alert on mount so screen readers announce it and keyboard
// users land on it, even when it is the very first thing rendered.
useEffect(() => {
containerRef.current?.focus();
}, []);
const message = error instanceof Error ? error.message : String(error);
return (
<div ref={containerRef} role="alert" tabIndex={-1} style={screenStyle}>
<h1 style={titleStyle}>PostHog Code failed to start</h1>
<p style={messageStyle}>{message}</p>
<button
type="button"
onClick={() => window.location.reload()}
style={buttonStyle}
>
Reload
</button>
</div>
);
}

interface BootErrorBoundaryProps {
children: React.ReactNode;
}

interface BootErrorBoundaryState {
error: Error | null;
}

export class BootErrorBoundary extends React.Component<
BootErrorBoundaryProps,
BootErrorBoundaryState
> {
state: BootErrorBoundaryState = { error: null };

static getDerivedStateFromError(error: Error): BootErrorBoundaryState {
return { error };
}

componentDidCatch(error: Error, info: React.ErrorInfo): void {
log.error("Renderer crashed during render", error, info.componentStack);
}

render(): React.ReactNode {
if (this.state.error) {
return <BootErrorScreen error={this.state.error} />;
}
return this.props.children;
}
}
43 changes: 32 additions & 11 deletions apps/code/src/renderer/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@ import "@renderer/di/container";
import "@renderer/platform-adapters/updates";
// Side effect: attaches window focus/visibility listeners so `focused` is accurate before inbox queries mount.
import "@posthog/ui/shell/rendererWindowFocusStore";
import {
BootErrorBoundary,
BootErrorScreen,
} from "@components/BootErrorBoundary";
import { Providers } from "@components/Providers";
import { preloadHighlighter } from "@pierre/diffs";
import { boot } from "@posthog/di/contribution";
import { ServiceProvider } from "@posthog/di/react";
import App from "@posthog/ui/shell/App";
import { logger } from "@posthog/ui/shell/logger";
import { initializePostHog } from "@posthog/ui/shell/posthogAnalyticsImpl";
import { registerDesktopContributions } from "@renderer/desktop-contributions";
import { container } from "@renderer/di/container";
Expand Down Expand Up @@ -80,18 +85,34 @@ if (bootstrapSessionId) {
initializePostHog(bootstrapSessionId);
}

registerDesktopContributions();
void boot(container);
const bootLog = logger.scope("renderer-boot");

const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Root element not found");

ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
<ServiceProvider container={container}>
<Providers>
<App />
</Providers>
</ServiceProvider>
</React.StrictMode>,
);
const root = ReactDOM.createRoot(rootElement);

try {
registerDesktopContributions();
boot(container).catch((error: unknown) => {
bootLog.error("Renderer boot sequence failed", error);
// Replaces the mounted tree without running effect cleanup; acceptable
// because a failed boot leaves the app unusable regardless.
root.render(<BootErrorScreen error={error} />);
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.

root.render(
<React.StrictMode>
<BootErrorBoundary>
<ServiceProvider container={container}>
<Providers>
<App />
</Providers>
</ServiceProvider>
</BootErrorBoundary>
</React.StrictMode>,
);
} catch (error) {
bootLog.error("Renderer failed to start", error);
root.render(<BootErrorScreen error={error} />);
}
Loading