Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/code-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,10 @@ jobs:
- name: Extract version from tag
id: version
shell: pwsh
env:
GITHUB_REF: ${{ github.ref }}
run: |
$tagVersion = "${{ github.ref }}" -replace "refs/tags/v", ""
$tagVersion = "$env:GITHUB_REF" -replace "refs/tags/v", ""
echo "Version: $tagVersion"
echo "version=$tagVersion" >> $env:GITHUB_OUTPUT

Expand Down
2 changes: 1 addition & 1 deletion apps/code/src/main/services/github-integration/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class GitHubIntegrationService extends TypedEventEmitter<GitHubIntegratio
): Promise<StartGitHubFlowOutput> {
try {
const cloudUrl = getCloudUrlFromRegion(region);
const nextPath = `/account/social-connected?provider=github&project_id=${projectId}&connect_from=posthog_code`;
const nextPath = `/account-connected/github-login?provider=github&project_id=${projectId}&connect_from=posthog_code`;
const authorizeUrl = `${cloudUrl}/api/environments/${projectId}/integrations/authorize/?kind=github&next=${encodeURIComponent(nextPath)}`;

this.clearFlowTimeout();
Expand Down
34 changes: 34 additions & 0 deletions apps/code/src/renderer/api/posthogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,40 @@ export class PostHogAPIClient {
return data.github_login;
}

/**
* `POST .../integrations/github/start/`. Optional `teamId` matches app project when session `current_team` differs.
*/
async startGithubUserIntegrationConnect(teamId?: number): Promise<{
install_url: string;
connect_flow?: "oauth_authorize" | "app_install";
}> {
const id = teamId ?? (await this.getTeamId());
const urlPath = `/api/users/@me/integrations/github/start/`;
const url = new URL(`${this.api.baseUrl}${urlPath}`);
const response = await this.api.fetcher.fetch({
method: "post",
url,
path: urlPath,
overrides: {
body: JSON.stringify({ team_id: id, connect_from: "posthog_code" }),
},
});
if (!response.ok) {
const err = (await response.json().catch(() => ({}))) as {
detail?: unknown;
};
const detail =
typeof err.detail === "string"
? err.detail
: "Failed to start GitHub connection";
throw new Error(detail);
}
return (await response.json()) as {
install_url: string;
connect_flow?: "oauth_authorize" | "app_install";
};
}

async switchOrganization(orgId: string): Promise<void> {
await this.api.patch("/api/users/{uuid}/", {
path: { uuid: "@me" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import {
isReportUpForReview,
} from "@features/inbox/utils/filterReports";
import { INBOX_REFETCH_INTERVAL_MS } from "@features/inbox/utils/inboxConstants";
import { useRepositoryIntegration } from "@hooks/useIntegrations";
import {
useIntegrations,
useRepositoryIntegration,
} from "@hooks/useIntegrations";
import { Box, Flex, ScrollArea } from "@radix-ui/themes";
import type { SignalReportsQueryParams } from "@shared/types";
import { useNavigationStore } from "@stores/navigationStore";
Expand Down Expand Up @@ -55,7 +58,15 @@ export function InboxSignalsTab() {
const { hasGithubIntegration } = useRepositoryIntegration();

// ── Signal source configs ───────────────────────────────────────────────
const { data: signalSourceConfigs } = useSignalSourceConfigs();
const { data: signalSourceConfigs, isPending: signalSourceConfigsPending } =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { data: signalSourceConfigs, isPending: signalSourceConfigsPending } =
const { isPending: signalSourceConfigsPending, data: signalSourceConfigs } =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'M SORRY LOL

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think formatter will complain here if we do this, but maybe not 🤔

useSignalSourceConfigs();
const { isPending: integrationsPending, data: integrationsData } =
useIntegrations();
/** Matches store-backed `hasGithubIntegration`, but uses query data so there is no lag behind the `useIntegrations` → Zustand sync effect. */
const hasGithubIntegrationFromQuery = useMemo(
() => integrationsData?.some((i) => i.kind === "github") ?? false,
[integrationsData],
);
const hasSignalSources = signalSourceConfigs?.some((c) => c.enabled) ?? false;
const enabledProducts = useMemo(() => {
const seen = new Set<string>();
Expand All @@ -78,6 +89,9 @@ export function InboxSignalsTab() {
const isInboxView = useNavigationStore((s) => s.view.type === "inbox");
const inboxPollingActive = windowFocused && isInboxView;

const inboxSourcesPrerequisitesLoaded =
!integrationsPending && !signalSourceConfigsPending;

// ── Data fetching ───────────────────────────────────────────────────────
useInboxAvailableSuggestedReviewers({
enabled: isInboxView,
Expand Down Expand Up @@ -121,6 +135,40 @@ export function InboxSignalsTab() {
staleTime: inboxPollingActive ? INBOX_REFETCH_INTERVAL_MS : 12_000,
});

const didAutoOpenSourcesDialogThisInboxVisitRef = useRef(false);

useEffect(() => {
if (!isInboxView) {
didAutoOpenSourcesDialogThisInboxVisitRef.current = false;
return;
}
if (!inboxSourcesPrerequisitesLoaded || isLoading || error != null) {
return;
}
if (totalCount <= 0) {
return;
}
const needsSourcesOrGithubSetup =
!hasSignalSources || !hasGithubIntegrationFromQuery;
if (!needsSourcesOrGithubSetup) {
return;
}
if (didAutoOpenSourcesDialogThisInboxVisitRef.current) {
return;
}
didAutoOpenSourcesDialogThisInboxVisitRef.current = true;
setSourcesDialogOpen(true);
}, [
isInboxView,
inboxSourcesPrerequisitesLoaded,
isLoading,
error,
totalCount,
hasSignalSources,
hasGithubIntegrationFromQuery,
setSourcesDialogOpen,
]);

const reports = useMemo(
() => filterReportsBySearch(allReports, searchQuery),
[allReports, searchQuery],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ export function InboxSourcesDialog({
}: InboxSourcesDialogProps) {
return (
<Dialog.Root open={open} onOpenChange={onOpenChange}>
<Dialog.Content maxWidth="520px">
<Dialog.Content maxWidth="800px">
<Flex align="center" justify="between" mb="3">
<Dialog.Title mb="0" className="text-base">
Signal sources
Inbox configuration
</Dialog.Title>
<Dialog.Close>
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,62 @@
import { Button } from "@components/ui/Button";
import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient";
import { useAuthStateValue } from "@features/auth/hooks/authQueries";
import { useAuthenticatedQuery } from "@hooks/useAuthenticatedQuery";
import { useRepositoryIntegration } from "@hooks/useIntegrations";
import {
ArrowSquareOutIcon,
GithubLogoIcon,
InfoIcon,
} from "@phosphor-icons/react";
import { Spinner } from "@radix-ui/themes";
import { trpcClient } from "@renderer/trpc/client";
import type { CloudRegion } from "@shared/types/regions";
import { getCloudUrlFromRegion } from "@shared/utils/urls";
import { queryClient } from "@utils/queryClient";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";

/** PostHog Cloud OAuth URL to attach GitHub (`connect_from` is handled by PostHog web after redirect). */
function posthogCloudGithubAccountLinkUrl(region: CloudRegion): string {
const url = new URL("/login/github/", getCloudUrlFromRegion(region));
url.searchParams.set("connect_from", "posthog_code");
return url.toString();
async function openUrlInBrowser(url: string): Promise<void> {
try {
await trpcClient.os.openExternal.mutate({ url });
} catch {
window.open(url, "_blank", "noopener,noreferrer");
}
}

/** Uses project-scoped integrations (see useRepositoryIntegration), not session `current_team`. */
export function GitHubConnectionBanner() {
const { data: githubLogin, isLoading } = useAuthenticatedQuery(
const { data: githubLogin, isLoading: loginLoading } = useAuthenticatedQuery(
["github_login"],
async (client) => client.getGithubLogin(),
{ staleTime: 5 * 60 * 1000 },
);
const { hasGithubIntegration: hasGithubForProject } =
useRepositoryIntegration();
const apiClient = useOptionalAuthenticatedClient();
const projectId = useAuthStateValue((s) => s.projectId);
const cloudRegion = useAuthStateValue((s) => s.cloudRegion);
const awaitingLink = useRef(false);
const connectInFlight = useRef(false);
const [connecting, setConnecting] = useState(false);

const canConnectCloud =
apiClient != null && projectId != null && cloudRegion != null;

// After the user clicks connect and returns to the app, refetch to pick up the new github_login
useEffect(() => {
const onFocus = () => {
if (awaitingLink.current) {
awaitingLink.current = false;
void queryClient.invalidateQueries({ queryKey: ["github_login"] });
void queryClient.invalidateQueries({
queryKey: ["integrations", "list"],
});
}
};
window.addEventListener("focus", onFocus);
return () => window.removeEventListener("focus", onFocus);
}, []);

if (isLoading) {
if (loginLoading) {
return null;
}

Expand All @@ -52,15 +68,28 @@ export function GitHubConnectionBanner() {
return null;
}

const connectUrl = posthogCloudGithubAccountLinkUrl(cloudRegion);
const label = connecting
? "Waiting for GitHub connection to complete in browser…"
: hasGithubForProject
? "Connect your GitHub profile to highlight what's relevant to you"
: "Connect your GitHub repo(s) to highlight what's relevant to you";

return (
<div className="pointer-events-auto absolute inset-x-2 bottom-2 z-20">
<div className="pointer-events-auto absolute inset-x-2 bottom-2 z-60">
<Button
type="button"
size="1"
variant="solid"
color="gray"
highContrast
disabled={!canConnectCloud || connecting}
disabledReason={
!canConnectCloud
? "Sign in to PostHog and select a cloud project."
: connecting
? "Finish the GitHub flow in your browser, then return to PostHog Code."
: null
}
className="h-fit w-full flex-wrap items-center justify-start gap-x-2 gap-y-1 whitespace-normal border-transparent bg-black py-1 text-left text-[12px] text-white shadow-none hover:bg-neutral-900"
tooltipContent={
<>
Expand All @@ -69,20 +98,60 @@ export function GitHubConnectionBanner() {
PostHog Code suggests report ownership using cutting-edge{" "}
<code>git blame</code> technology.
<br />
For relevant reports, connect your GitHub profile.
{hasGithubForProject
? "You'll authorize with GitHub in the browser via PostHog Cloud to link your profile."
: "You'll connect GitHub via PostHog Cloud (GitHub App or quick OAuth—depending on your setup)."}{" "}
Your identity is stored as a GitHub UserIntegration so Code can
highlight relevant work.
</div>
</>
}
onClick={() => {
if (!canConnectCloud || connectInFlight.current) {
return;
}
connectInFlight.current = true;
awaitingLink.current = true;
void trpcClient.os.openExternal.mutate({ url: connectUrl });
setConnecting(true);
void (async () => {
try {
const res =
await apiClient.startGithubUserIntegrationConnect(projectId);
const installUrl = res.install_url?.trim() ?? "";
if (!installUrl) {
awaitingLink.current = false;
toast.error(
"GitHub connection did not return a URL. Please try again.",
);
return;
}
await openUrlInBrowser(installUrl);
} catch (e) {
awaitingLink.current = false;
toast.error(
e instanceof Error
? e.message
: "Failed to start GitHub connection",
);
} finally {
connectInFlight.current = false;
setConnecting(false);
}
})();
}}
>
<GithubLogoIcon className="flex-none" size={12} />
<span className="min-w-0 flex-1 basis-0 text-balance">
Connect your GitHub profile to highlight what's relevant to you
</span>
<ArrowSquareOutIcon className="flex-none" size={11} />
{connecting ? (
<>
<Spinner size="1" className="shrink-0 text-current" />
<span className="min-w-0 flex-1 basis-0 text-balance">{label}</span>
</>
) : (
<>
<GithubLogoIcon className="flex-none" size={12} />
<span className="min-w-0 flex-1 basis-0 text-balance">{label}</span>
<ArrowSquareOutIcon className="flex-none" size={11} />
</>
)}
</Button>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ export function GitHubIntegrationSection({
}
}, [hasGithubIntegration, connecting, stopPolling]);

// Fallback for when the `posthog-code://integration` deep link from PostHog Cloud
// never makes it back to the app (browser blocked the protocol prompt, focus didn't
// return cleanly, etc.). The integrations query has a 5-minute staleTime so the
// global `refetchOnWindowFocus: true` won't refetch it on its own — invalidate
// explicitly while a connect flow is in flight.
useEffect(() => {
if (!connecting) return;
const handleFocus = () => invalidateIntegrations();
window.addEventListener("focus", handleFocus);
return () => window.removeEventListener("focus", handleFocus);
}, [connecting, invalidateIntegrations]);

useGitHubIntegrationCallback({
onSuccess: () => {
stopPolling();
Expand Down Expand Up @@ -111,7 +123,7 @@ export function GitHubIntegrationSection({
</Box>
<Flex direction="column">
<Text className="font-medium text-(--gray-12) text-sm">
Code access
Project-level code access
</Text>
{hasGithubIntegration &&
!isLoadingRepos &&
Expand Down
Loading