diff --git a/packages/shared/src/analytics-events.ts b/packages/shared/src/analytics-events.ts index adac93086a..5abd335c03 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 c8cb744323..9edc78fc8f 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 0bc10d6f92..143cce1daf 100644 --- a/packages/ui/src/features/canvas/components/FeedbackModal.tsx +++ b/packages/ui/src/features/canvas/components/FeedbackModal.tsx @@ -10,16 +10,37 @@ 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 = + { + 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 +48,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 +66,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 +97,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 +122,7 @@ function FeedbackModalForm({ + + + diff --git a/packages/ui/src/shell/analytics.ts b/packages/ui/src/shell/analytics.ts index 7768ca64c8..011ddc64e9 100644 --- a/packages/ui/src/shell/analytics.ts +++ b/packages/ui/src/shell/analytics.ts @@ -32,8 +32,7 @@ export interface AnalyticsTracker { resetUser(): void; captureSurveyResponse(params: { surveyId: string; - questionId: string; - response: string; + responses: Array<{ questionId: string; response: string }>; }): void; } @@ -82,8 +81,7 @@ export function resetUser(): void { export function captureSurveyResponse(params: { surveyId: string; - questionId: string; - response: string; + responses: Array<{ questionId: string; response: string }>; }): void { resolveService(ANALYTICS_TRACKER).captureSurveyResponse( params, diff --git a/packages/ui/src/shell/posthogAnalyticsImpl.ts b/packages/ui/src/shell/posthogAnalyticsImpl.ts index ab3d65b768..32a39fb6ae 100644 --- a/packages/ui/src/shell/posthogAnalyticsImpl.ts +++ b/packages/ui/src/shell/posthogAnalyticsImpl.ts @@ -219,31 +219,37 @@ export function track( } /** - * Record a survey response via posthog-js's `survey sent` event. The survey - * must already exist (and be launched) in the project the app reports to, or - * the response will not attach to it. + * Record a survey response via posthog-js's `survey sent` event. Pass one entry + * per answered question; they're submitted together as a single response. The + * survey must already exist (and be launched) in the project the app reports to, + * or the response will not attach to it. */ export function captureSurveyResponse({ surveyId, - questionId, - response, + responses, }: { surveyId: string; - questionId: string; - response: string; + responses: Array<{ questionId: string; response: string }>; }) { if (!isInitialized) { return; } - posthog.capture("survey sent", { + const properties: Record = { $survey_id: surveyId, - $survey_questions: [{ id: questionId }], - // Newer ingestion keys responses by question id; `$survey_response` is the - // legacy single-question key. Send both so the response attaches either way. - [`$survey_response_${questionId}`]: response, - $survey_response: response, - }); + $survey_questions: responses.map(({ questionId }) => ({ id: questionId })), + }; + // Newer ingestion keys each response by question id. + for (const { questionId, response } of responses) { + properties[`$survey_response_${questionId}`] = response; + } + // `$survey_response` is the legacy single-question key; only set it when there + // is exactly one answer, otherwise it would be ambiguous. + if (responses.length === 1) { + properties.$survey_response = responses[0].response; + } + + posthog.capture("survey sent", properties); } /**