Files
Nik Richers e3c677fc5a 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>
2026-09-20 17:08:03 +00:00

109 lines
2.8 KiB
TypeScript

'use client'
import {
getMonitoringAgent,
getMonitoringAgentHarnesses,
type MonitoringAgentHarnessSetup,
} from '~/data/monitoring-agents.utils'
import { Sparkles } from 'lucide-react'
import { useTheme } from 'next-themes'
import { type ReactNode } from 'react'
import ReactMarkdown from 'react-markdown'
import { ConnectionIcon } from 'ui-patterns/McpUrlBuilder'
import { AiPrompt } from './AiPrompt'
import { PromptCode } from './PromptPanel'
import { TabPanel, Tabs } from './Tabs'
type AgentSetupProps = {
id: string
}
const markdownComponents = {
p: ({ children }: { children?: ReactNode }) => <>{children}</>,
a: ({ href, children }: { href?: string; children?: ReactNode }) => {
if (!href) return <>{children}</>
const external = /^(?:[a-z][a-z0-9+\-.]*:|\/\/)/i.test(href)
return (
<a
href={href}
className="text-brand-link hover:underline"
{...(external ? { target: '_blank', rel: 'noreferrer noopener' } : {})}
>
{children}
</a>
)
},
code: PromptCode,
}
function HarnessBody({ harness }: { harness: MonitoringAgentHarnessSetup }) {
return (
<>
<p>{harness.intro}</p>
<ol>
{harness.steps.map((step) => (
<li key={step}>
<ReactMarkdown components={markdownComponents}>{step}</ReactMarkdown>
</li>
))}
</ol>
{harness.note && (
<p>
<ReactMarkdown components={markdownComponents}>{harness.note}</ReactMarkdown>
</p>
)}
<p>
<a
href={harness.docsUrl}
className="text-brand-link hover:underline"
target="_blank"
rel="noreferrer noopener"
>
{harness.label} docs
</a>
</p>
</>
)
}
function AgentSetup({ id }: AgentSetupProps) {
const agent = getMonitoringAgent(id)
const harnesses = getMonitoringAgentHarnesses(agent)
const { resolvedTheme } = useTheme()
const theme = resolvedTheme?.includes('dark') ? 'dark' : 'light'
return (
<Tabs
defaultActiveId="prompt"
type="underlined"
size="small"
wrappable
queryGroup="agent-setup"
>
<TabPanel id="prompt" label="Prompt" icon={<Sparkles size={14} />}>
<AiPrompt id={agent.promptId} telemetry={{ source: 'agent_setup' }} />
</TabPanel>
{harnesses.map((harness) => (
<TabPanel
key={harness.key}
id={harness.key}
label={harness.label}
icon={
<ConnectionIcon
theme={theme}
connection={harness.icon}
hasDistinctDarkIcon={harness.hasDistinctDarkIcon}
/>
}
>
<HarnessBody harness={harness} />
</TabPanel>
))}
</Tabs>
)
}
export { AgentSetup }
export type { AgentSetupProps }