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