mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
1 parent
2a75ff7ae6
commit
e3c677fc5a
8 files changed
+89
-23
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?.()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user