From e3c677fc5af311dcdc59b7810f35a64af0893e55 Mon Sep 17 00:00:00 2001 From: Nik Richers Date: Sun, 20 Sep 2026 10:08:03 -0700 Subject: [PATCH] feat(docs): track prompt panel copies in PostHog (#50482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Add telemetry for `PromptPanel` to help us understand how people interact with our AI prompts better. Relates to DOCS-1393 Dashboard(restricted access): [Docs: AI prompt affordances](https://eu.posthog.com/project/34344/dashboard/957235) ## What is the current behavior? The docs homepage cover renders a setup panel with "AI Prompt" and "CLI" tabs, and guides render `AiPrompt` blocks. Both are built on the shared `PromptPanel`, whose copy button called `copyToClipboard` and nothing else. Copying was therefore unmeasured, while the neighbouring affordances (`ask_ai_clicked`, `agent_setup_clicked`, `copy_as_markdown_clicked`) are already instrumented. ## What is the new behavior? `PromptPanel` takes an optional `telemetry` prop. When it is set, the panel sends a new docs-owned event after a **successful** clipboard write, so instrumentation lives in the shared component instead of a forked homepage copy button. New event in `packages/common/telemetry-constants.ts`: | | | | --- | --- | | `action` | `docs_ai_prompt_copied` | | `source` | `homepage` \| `guide` \| `agent_setup` | | `tab` | `prompt` \| `cli` (omitted for panes outside that set) | | `promptId` | prompt id, when the panel comes from an `AiPrompt` block | Wired consumers: `HomePageCover` (`homepage`), `AiPrompt` (`guide` by default, plus `promptId`), and `AgentSetup` (`agent_setup`). No prompt body text and no PII is sent. Studio's existing `ai_prompt_copied` event is deliberately left alone: it has a different owner and surface, and merging the two would blend unrelated funnels. ### Proof it works ``` $ pnpm run test:local:unwatch features/ui/PromptPanel.telemetry.test.ts RUN v5.0.0 /apps/docs Test Files 1 passed (1) Tests 4 passed (4) Duration 775ms ``` ## Additional context Test plan, run against a local docs server with a stub telemetry endpoint so the request bodies could be read directly: | Case | Observed payload | | --- | --- | | Homepage, AI Prompt tab | `{"source":"homepage","tab":"prompt"}` | | Homepage, CLI tab | `{"source":"homepage","tab":"cli"}` | | Next.js quickstart `AiPrompt` | `{"source":"guide","tab":"prompt","promptId":"nextjs"}` | | `automate-with-agents/health` `AgentSetup` | `{"source":"agent_setup","tab":"prompt","promptId":"monitoring-agent-health"}` | | Clipboard write rejected | no request sent, error toast shown, button does not flip to "copied" | The failure case was re-checked with a control click on the same page after restoring a working clipboard, which did send the event, so the negative result is not just a missed handler. Also run: `turbo typecheck --filter=docs --filter=common` (passes), Prettier check on the touched files (passes), and ESLint on the touched docs files (no new findings; the one warning on `HomePageCover` is the pre-existing default export). ## Summary by CodeRabbit * **New Features** * Successful prompt copies are now tracked across the homepage, documentation guides, and agent setup experiences. * Copy activity records the prompt’s source, selected format, and associated prompt when available, providing more complete usage insights. --------- Co-authored-by: Nik Richers --- apps/docs/components/HomePageCover.tsx | 2 +- apps/docs/features/docs/MdxBase.shared.tsx | 6 ++- apps/docs/features/ui/AgentSetup.tsx | 2 +- apps/docs/features/ui/AiPrompt.tsx | 7 +++- .../ui/CodeBlock/CodeBlock.client.tsx | 4 ++ apps/docs/features/ui/PromptPanel.tsx | 40 ++++++++++++++++++- apps/docs/lib/content-listings.test.ts | 15 ------- packages/common/telemetry-constants.ts | 36 +++++++++++++++++ 8 files changed, 89 insertions(+), 23 deletions(-) diff --git a/apps/docs/components/HomePageCover.tsx b/apps/docs/components/HomePageCover.tsx index 998b8b66236..3b6179f4c4d 100644 --- a/apps/docs/components/HomePageCover.tsx +++ b/apps/docs/components/HomePageCover.tsx @@ -19,7 +19,7 @@ const fullGettingStartedEnabled = isFeatureEnabled('docs:full_getting_started') function SetupPrompt({ cliCode }: { cliCode: ReactNode }) { return ( - + Agent Prompt {setupPrompt} diff --git a/apps/docs/features/docs/MdxBase.shared.tsx b/apps/docs/features/docs/MdxBase.shared.tsx index ee38ff457ab..3e52104405c 100644 --- a/apps/docs/features/docs/MdxBase.shared.tsx +++ b/apps/docs/features/docs/MdxBase.shared.tsx @@ -42,7 +42,7 @@ import SqlToRest from 'ui-patterns/SqlToRest' import { AgentPluginsPanel } from '../ui/AgentPluginsPanel' import { AgentSetup } from '../ui/AgentSetup' import { AgentWatchSchedule } from '../ui/AgentWatchSchedule' -import { AiPrompt } from '../ui/AiPrompt' +import { AiPrompt, type AiPromptProps } from '../ui/AiPrompt' import { ErrorCodes } from '../ui/ErrorCodes' import { McpConfigPanel } from '../ui/McpConfigPanel' @@ -74,7 +74,9 @@ const components = { AgentPluginsPanel, AgentSetup, AgentWatchSchedule, - AiPrompt, + AiPrompt: (props: Omit) => ( + + ), AiPromptsIndex, AiSkillsIndex, AuthSmsProviderConfig, diff --git a/apps/docs/features/ui/AgentSetup.tsx b/apps/docs/features/ui/AgentSetup.tsx index 9bb457fc406..66f5f498f89 100644 --- a/apps/docs/features/ui/AgentSetup.tsx +++ b/apps/docs/features/ui/AgentSetup.tsx @@ -82,7 +82,7 @@ function AgentSetup({ id }: AgentSetupProps) { queryGroup="agent-setup" > }> - + {harnesses.map((harness) => ( } /** @@ -27,14 +30,14 @@ type AiPromptProps = { * Prompt text lives in `~/data/ai-prompts.data`. Markdown export is opt-in so * existing quickstarts do not duplicate their instructions in bulk exports. */ -function AiPrompt({ id }: AiPromptProps) { +function AiPrompt({ id, telemetry }: AiPromptProps) { const prompt = aiPrompts[id as AiPromptId] if (!prompt) { throw new Error(`Unknown AiPrompt id: ${id}`) } return ( - + Agent Prompt {prompt} diff --git a/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx b/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx index 9e2b0822f61..ce42fa22472 100644 --- a/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx +++ b/apps/docs/features/ui/CodeBlock/CodeBlock.client.tsx @@ -185,11 +185,14 @@ export function CodeCopyButton({ content, label = 'Copy code', copiedLabel = 'Code copied', + onCopied, }: { className?: string content: string label?: string copiedLabel?: string + /** Runs after a successful clipboard write. */ + onCopied?: () => void }) { const [copied, setCopied] = useState(false) @@ -203,6 +206,7 @@ export function CodeCopyButton({ const handleCopy = async () => { copyToClipboard(content, () => { setCopied(true) + onCopied?.() }) } diff --git a/apps/docs/features/ui/PromptPanel.tsx b/apps/docs/features/ui/PromptPanel.tsx index 352849e1c71..3920c8494fe 100644 --- a/apps/docs/features/ui/PromptPanel.tsx +++ b/apps/docs/features/ui/PromptPanel.tsx @@ -1,5 +1,7 @@ 'use client' +import { useSendTelemetryEvent } from '~/lib/telemetry' +import { type DocsAiPromptSource } from 'common/telemetry-constants' import { ChevronDown } from 'lucide-react' import { Children, isValidElement, useId, useState, type ReactElement, type ReactNode } from 'react' import ReactMarkdown from 'react-markdown' @@ -30,9 +32,21 @@ type PromptProps = { expandable?: boolean } +type PromptPanelTelemetry = { + /** Surface the panel renders on. */ + source: DocsAiPromptSource + /** Prompt identifier, when the panel wraps a known prompt. */ + promptId?: string +} + type PromptPanelProps = { children: ReactNode className?: string + /** + * Sends `docs_ai_prompt_copied` on a successful copy. Omit to leave the panel + * untracked. + */ + telemetry?: PromptPanelTelemetry } type CollectedPrompt = { @@ -244,12 +258,13 @@ const tabTriggerClassName = cn( * * ``` */ -function PromptPanel({ children, className }: PromptPanelProps) { +function PromptPanel({ children, className, telemetry }: PromptPanelProps) { const fallbackId = useId() const titleId = useId() const prompts = collectPrompts(children) const [activeTab, setActiveTab] = useState(prompts[0]?.value ?? fallbackId) const [shimmerEnabled, setShimmerEnabled] = useState(true) + const sendTelemetryEvent = useSendTelemetryEvent() if (prompts.length === 0) return null @@ -258,6 +273,19 @@ function PromptPanel({ children, className }: PromptPanelProps) { const dismissShimmer = () => setShimmerEnabled(false) const copyTarget = typeof activePrompt.title === 'string' ? activePrompt.title : 'content' + const handleCopied = telemetry + ? () => { + sendTelemetryEvent({ + action: 'docs_ai_prompt_copied', + properties: { + source: telemetry.source, + tab: activePrompt.value, + promptId: telemetry.promptId, + }, + }) + } + : undefined + const header = (
{hasTabs ? ( @@ -285,6 +313,7 @@ function PromptPanel({ children, className }: PromptPanelProps) { content={activePrompt.copyValue} label={`Copy ${copyTarget}`} copiedLabel={`${copyTarget} copied`} + onCopied={handleCopied} />
) @@ -347,4 +376,11 @@ function PromptPanel({ children, className }: PromptPanelProps) { } export { Prompt, PromptCode, PromptContent, PromptCopy, PromptMarkdown, PromptPanel, PromptTitle } -export type { PromptContentProps, PromptCopyProps, PromptPanelProps, PromptProps, PromptTitleProps } +export type { + PromptContentProps, + PromptCopyProps, + PromptPanelProps, + PromptPanelTelemetry, + PromptProps, + PromptTitleProps, +} diff --git a/apps/docs/lib/content-listings.test.ts b/apps/docs/lib/content-listings.test.ts index 7d017438edf..1eeb4059884 100644 --- a/apps/docs/lib/content-listings.test.ts +++ b/apps/docs/lib/content-listings.test.ts @@ -355,18 +355,3 @@ describe('contentListingItemSchema icon', () => { expect(result.success).toBe(false) }) }) - -describe('TelemetryEvent union', () => { - it('includes docs_content_listing_clicked', () => { - const event = { - action: 'docs_content_listing_clicked' as const, - properties: { - targetPath: '/guides/storage', - linkTitle: 'Storage', - }, - } - - const _typeCheck: import('common/telemetry-constants').TelemetryEvent = event - expect(_typeCheck.action).toBe('docs_content_listing_clicked') - }) -}) diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index eaf258f118a..a97bf198f03 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -994,6 +994,41 @@ export interface AskAiClickedEvent { } } +/** + * Surface that rendered the prompt panel a user copied from. + */ +export type DocsAiPromptSource = 'homepage' | 'guide' | 'agent_setup' + +/** + * User copied the contents of a docs prompt panel - the homepage setup card or an + * `AiPrompt` block - and the clipboard write succeeded. Fires on success only; + * failed clipboard writes are not counted. + * + * Distinct from `ai_prompt_copied`, which belongs to Studio's AI assistant. + * + * @group Events + * @source docs + * @page /docs, /docs/guides + */ +export interface DocsAiPromptCopiedEvent { + action: 'docs_ai_prompt_copied' + properties: { + /** + * Surface the panel was rendered on. + */ + source: DocsAiPromptSource + /** + * `value` of the pane that was active when the copy happened. Known panes + * are `prompt` and `cli`; other strings remain allowed for future panes. + */ + tab: 'prompt' | 'cli' | (string & {}) + /** + * Prompt identifier, set when the panel comes from an `AiPrompt` block. + */ + promptId?: string + } +} + /** * User clicked a curated orientation link from a content listings MDX component. * @@ -3964,6 +3999,7 @@ export type TelemetryEvent = | CopyAsMarkdownClickedEvent | AgentSetupClickedEvent | AskAiClickedEvent + | DocsAiPromptCopiedEvent | DocsContentListingClickedEvent | Docs404RecommendationClickedEvent | DocsProjectConfigVariablesCopyButtonClickedEvent