From 8a029774457043bf49005e390326c3175805e00a Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Sat, 20 Jun 2026 13:25:03 -0700 Subject: [PATCH 1/3] Add "PostHog Web" button to Bluebird top bar Adds a region-aware "PostHog Web" button with an external-link icon pinned to the far right of the Channels (project-bluebird) title bar. Clicking it opens the user's current project in the browser at https://.posthog.com/project/, derived from cloudRegion, falling back to the account root when no project is set. Reuses existing utilities (getPostHogUrl, openUrlInBrowser, useAuthStateValue); no new helpers added. Generated-By: PostHog Code Task-Id: 146f5091-189a-4a5f-8456-0d9157ad48b1 --- packages/ui/src/router/routes/__root.tsx | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/ui/src/router/routes/__root.tsx b/packages/ui/src/router/routes/__root.tsx index 760d96d3d..0a1ef76b1 100644 --- a/packages/ui/src/router/routes/__root.tsx +++ b/packages/ui/src/router/routes/__root.tsx @@ -1,3 +1,4 @@ +import { ArrowSquareOut } from "@phosphor-icons/react"; import { useHostTRPC, useHostTRPCClient } from "@posthog/host-router/react"; import { Button } from "@posthog/quill"; import { @@ -6,6 +7,7 @@ import { PROJECT_BLUEBIRD_FLAG, SYNC_CLOUD_TASKS_FLAG, } from "@posthog/shared"; +import { useAuthStateValue } from "@posthog/ui/features/auth/store"; import { UsageLimitModal } from "@posthog/ui/features/billing/UsageLimitModal"; import { ChannelsSidebar } from "@posthog/ui/features/canvas/components/ChannelsSidebar"; import { @@ -39,6 +41,8 @@ import { logger } from "@posthog/ui/shell/logger"; import { onFeatureFlagsLoaded } from "@posthog/ui/shell/posthogAnalyticsImpl"; import { SpaceSwitcher } from "@posthog/ui/shell/SpaceSwitcher"; import { useShortcutsSheetStore } from "@posthog/ui/shell/shortcutsSheetStore"; +import { openUrlInBrowser } from "@posthog/ui/utils/browser"; +import { getPostHogUrl } from "@posthog/ui/utils/urls"; import { Box, Flex } from "@radix-ui/themes"; import { useQueryClient } from "@tanstack/react-query"; import { @@ -106,6 +110,17 @@ function RootLayout() { setFeedbackMode(null); if (wasLeaving) navigate({ to: "/code" }); }; + + // "PostHog Web" button in the Channels title bar: opens the user's current + // project on the correct cloud (region comes from cloudRegion via + // getPostHogUrl), falling back to the account root when no project is set. + const currentProjectId = useAuthStateValue((s) => s.currentProjectId); + const handleOpenPostHogWeb = () => { + const url = getPostHogUrl( + currentProjectId ? `/project/${currentProjectId}` : "/", + ); + if (url) void openUrlInBrowser(url); + }; const { isOpen: commandMenuOpen, setOpen: setCommandMenuOpen, @@ -246,6 +261,16 @@ function RootLayout() { Leave feedback + + + From 38a8fcd2f1f6061d1fa71567524e7fe8f4a3f587 Mon Sep 17 00:00:00 2001 From: Raquel Smith Date: Sat, 20 Jun 2026 13:42:01 -0700 Subject: [PATCH 2/3] Gate PostHog Web button behind feedback modal, capture source Clicking "PostHog Web" now opens the feedback modal first and navigates only once it's submitted or skipped, asking "Why are you going back to PostHog web?". The modal prompt is rendered at full contrast so the question is more visible in every mode. Also fires a "PostHog web opened" analytics event on click, and records where feedback came from (generic / going back to Code / visiting PostHog web) via a new single-choice survey question submitted alongside the open-text answer. Generated-By: PostHog Code Task-Id: 146f5091-189a-4a5f-8456-0d9157ad48b1 --- packages/shared/src/analytics-events.ts | 2 + .../canvas/components/FeedbackModal.test.tsx | 12 +++-- .../canvas/components/FeedbackModal.tsx | 54 ++++++++++++++----- .../ui/src/features/canvas/feedbackSurvey.ts | 18 ++++++- packages/ui/src/router/routes/__root.tsx | 34 +++++++----- packages/ui/src/shell/analytics.ts | 6 +-- packages/ui/src/shell/posthogAnalyticsImpl.ts | 34 +++++++----- 7 files changed, 114 insertions(+), 46 deletions(-) diff --git a/packages/shared/src/analytics-events.ts b/packages/shared/src/analytics-events.ts index adac93086..5abd335c0 100644 --- a/packages/shared/src/analytics-events.ts +++ b/packages/shared/src/analytics-events.ts @@ -844,6 +844,7 @@ export const ANALYTICS_EVENTS = { COMMAND_MENU_ACTION: "Command menu action", COMMAND_CENTER_VIEWED: "Command center viewed", SKILL_BUTTON_TRIGGERED: "Skill button triggered", + POSTHOG_WEB_OPENED: "PostHog web opened", // Permission events PERMISSION_RESPONDED: "Permission responded", @@ -978,6 +979,7 @@ export type EventPropertyMap = { [ANALYTICS_EVENTS.COMMAND_MENU_ACTION]: CommandMenuActionProperties; [ANALYTICS_EVENTS.COMMAND_CENTER_VIEWED]: never; [ANALYTICS_EVENTS.SKILL_BUTTON_TRIGGERED]: SkillButtonTriggeredProperties; + [ANALYTICS_EVENTS.POSTHOG_WEB_OPENED]: never; // Permission events [ANALYTICS_EVENTS.PERMISSION_RESPONDED]: PermissionRespondedProperties; diff --git a/packages/ui/src/features/canvas/components/FeedbackModal.test.tsx b/packages/ui/src/features/canvas/components/FeedbackModal.test.tsx index c8cb74432..9edc78fc8 100644 --- a/packages/ui/src/features/canvas/components/FeedbackModal.test.tsx +++ b/packages/ui/src/features/canvas/components/FeedbackModal.test.tsx @@ -27,6 +27,7 @@ describe("FeedbackModal", () => { it.each([ { mode: "leaving" as const, expected: "Skip", missing: "Cancel" }, + { mode: "posthog-web" as const, expected: "Skip", missing: "Cancel" }, { mode: "feedback" as const, expected: "Cancel", missing: "Skip" }, ])( "shows the $expected secondary button in $mode mode", @@ -52,9 +53,9 @@ describe("FeedbackModal", () => { expect(submit).not.toHaveAttribute("aria-disabled", "true"); }); - it("captures the trimmed response and finishes on submit", async () => { + it("captures the trimmed response with its source and finishes on submit", async () => { const user = userEvent.setup(); - const onFinished = renderModal("leaving"); + const onFinished = renderModal("posthog-web"); await user.type( screen.getByPlaceholderText("Share your feedback"), @@ -64,7 +65,12 @@ describe("FeedbackModal", () => { expect(captureSurveyResponse).toHaveBeenCalledTimes(1); expect(captureSurveyResponse).toHaveBeenCalledWith( - expect.objectContaining({ response: "great work" }), + expect.objectContaining({ + responses: [ + expect.objectContaining({ response: "great work" }), + expect.objectContaining({ response: "Visiting PostHog web" }), + ], + }), ); expect(onFinished).toHaveBeenCalledTimes(1); }); diff --git a/packages/ui/src/features/canvas/components/FeedbackModal.tsx b/packages/ui/src/features/canvas/components/FeedbackModal.tsx index 0bc10d6f9..74838e74b 100644 --- a/packages/ui/src/features/canvas/components/FeedbackModal.tsx +++ b/packages/ui/src/features/canvas/components/FeedbackModal.tsx @@ -10,16 +10,39 @@ import { Textarea, } from "@posthog/quill"; import { + FEEDBACK_SOURCE_BY_MODE, FEEDBACK_SURVEY_ID, FEEDBACK_SURVEY_QUESTION_ID, + FEEDBACK_SURVEY_SOURCE_QUESTION_ID, } from "@posthog/ui/features/canvas/feedbackSurvey"; import { captureSurveyResponse } from "@posthog/ui/shell/analytics"; import { useState } from "react"; -export type FeedbackModalMode = "feedback" | "leaving"; +export type FeedbackModalMode = "feedback" | "leaving" | "posthog-web"; + +/** Title + prompt shown for each way the modal can be opened. */ +const MODAL_COPY: Record< + FeedbackModalMode, + { title: string; prompt: string } +> = { + feedback: { + title: "Leave feedback", + prompt: + "How's the Channels experience? Tell us what's working and what you'd change.", + }, + leaving: { + title: "Before you go back to Code", + prompt: + "How's the Channels experience? Tell us what's working and what you'd change.", + }, + "posthog-web": { + title: "Before you head to PostHog web", + prompt: "Why are you going back to PostHog web?", + }, +}; export interface FeedbackModalProps { - /** `null` closes the modal. `"leaving"` shows a Skip button, `"feedback"` a Cancel button. */ + /** `null` closes the modal. `"feedback"` shows a Cancel button; the navigation-intercept modes (`"leaving"`, `"posthog-web"`) show a Skip button. */ mode: FeedbackModalMode | null; /** Called after the response is submitted, and when the modal is skipped/cancelled/dismissed. */ onFinished: () => void; @@ -27,9 +50,10 @@ export interface FeedbackModalProps { /** * Feedback modal for the Channels space. Submitting records the text as a - * PostHog survey response (see {@link FEEDBACK_SURVEY_ID}). The secondary button - * reads "Skip" when opened by "Go back to Code" (`mode === "leaving"`) and - * "Cancel" when opened by "Leave feedback". + * PostHog survey response (see {@link FEEDBACK_SURVEY_ID}) along with where the + * modal was opened from. The secondary button reads "Skip" when the modal + * intercepts a navigation (`"leaving"` / `"posthog-web"`) and "Cancel" when + * opened directly by "Leave feedback". */ export function FeedbackModal({ mode, onFinished }: FeedbackModalProps) { const open = mode !== null; @@ -44,10 +68,11 @@ export function FeedbackModal({ mode, onFinished }: FeedbackModalProps) { > - Leave feedback - - How's the Channels experience? Tell us what's working and what you'd - change. + {mode ? MODAL_COPY[mode].title : ""} + {/* The prompt is the question we want answered, so render it at full + contrast rather than the muted default. */} + + {mode ? MODAL_COPY[mode].prompt : ""} {/* Mounted only while open so the textarea resets on each open without @@ -74,8 +99,13 @@ function FeedbackModalForm({ if (!response) return; captureSurveyResponse({ surveyId: FEEDBACK_SURVEY_ID, - questionId: FEEDBACK_SURVEY_QUESTION_ID, - response, + responses: [ + { questionId: FEEDBACK_SURVEY_QUESTION_ID, response }, + { + questionId: FEEDBACK_SURVEY_SOURCE_QUESTION_ID, + response: FEEDBACK_SOURCE_BY_MODE[mode], + }, + ], }); onFinished(); }; @@ -94,7 +124,7 @@ function FeedbackModalForm({