feat(docs): track prompt panel copies in PostHog (#50482)

## 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).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Nik Richers <nik@validmind.ai>
This commit is contained in:
Nik RichersandNik Richers authored and GitHub committed 2026-09-20 17:08:03 +00:00
1 parent 2a75ff7ae6
commit e3c677fc5a
8 files changed
+89 -23

No files matched your search

+1 -1
View File
@@ -19,7 +19,7 @@ const fullGettingStartedEnabled = isFeatureEnabled('docs:full_getting_started')
function SetupPrompt({ cliCode }: { cliCode: ReactNode }) {
return (
<PromptPanel>
<PromptPanel telemetry={{ source: 'homepage' }}>
<Prompt value="prompt" expandable>
<PromptTitle>Agent Prompt</PromptTitle>
<PromptCopy>{setupPrompt}</PromptCopy>
+4 -2
View File
@@ -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<AiPromptProps, 'telemetry'>) => (
<AiPrompt {...props} telemetry={{ source: 'guide' }} />
),
AiPromptsIndex,
AiSkillsIndex,
AuthSmsProviderConfig,
+1 -1
View File
@@ -82,7 +82,7 @@ function AgentSetup({ id }: AgentSetupProps) {
queryGroup="agent-setup"
>
<TabPanel id="prompt" label="Prompt" icon={<Sparkles size={14} />}>
<AiPrompt id={agent.promptId} />
<AiPrompt id={agent.promptId} telemetry={{ source: 'agent_setup' }} />
</TabPanel>
{harnesses.map((harness) => (
<TabPanel
+5 -2
View File
@@ -9,6 +9,7 @@ import {
PromptMarkdown,
PromptPanel,
PromptTitle,
type PromptPanelTelemetry,
} from './PromptPanel'
type AiPromptProps = {
@@ -16,6 +17,8 @@ type AiPromptProps = {
id: AiPromptId | string
/** Includes the prompt body in generated guide Markdown. */
includeInMarkdown?: boolean
/** Surface reported when the prompt is copied. */
telemetry: Omit<PromptPanelTelemetry, 'promptId'>
}
/**
@@ -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 (
<PromptPanel>
<PromptPanel telemetry={{ ...telemetry, promptId: id }}>
<Prompt value="prompt" expandable>
<PromptTitle>Agent Prompt</PromptTitle>
<PromptCopy>{prompt}</PromptCopy>
@@ -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?.()
})
}
+38 -2
View File
@@ -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(
* </PromptPanel>
* ```
*/
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 = (
<div className="flex h-11 items-center justify-between pl-4 pr-2 shadow-[inset_0_-1px_0_0_var(--border-default)] [--btn-active:color-mix(in_srgb,var(--foreground)_4%,var(--background-200))]">
{hasTabs ? (
@@ -285,6 +313,7 @@ function PromptPanel({ children, className }: PromptPanelProps) {
content={activePrompt.copyValue}
label={`Copy ${copyTarget}`}
copiedLabel={`${copyTarget} copied`}
onCopied={handleCopied}
/>
</div>
)
@@ -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,
}
-15
View File
@@ -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')
})
})
+36
View File
@@ -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