diff --git a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx index 0de0a72edf0..2444358fc79 100644 --- a/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx +++ b/apps/studio/components/layouts/ProjectLayout/LayoutHeader/LayoutHeader.tsx @@ -217,7 +217,7 @@ const LayoutHeader = ({
{customHeaderComponents && customHeaderComponents} diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 6159f03f105..209793bcdf0 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -1964,7 +1964,29 @@ export interface CommandMenuOpenedEvent { */ trigger: 'keyboard_shortcut' | 'search_input' /** - * The trigger that opened the command menu + * In which app the command input was typed + */ + app: 'studio' | 'docs' | 'www' + } + groups: TelemetryGroups +} + +/** + * User typed a search term in the command menu input. + * + * @group Events + * @source ui-patterns + * @page any + */ +export interface CommandInputTypedEvent { + action: 'command_input_typed' + properties: { + /** + * Search term typed into the command menu input + */ + value: string + /** + * In which app the command input was typed */ app: 'studio' | 'docs' | 'www' } @@ -2086,3 +2108,4 @@ export type TelemetryEvent = | TableRLSEnabledEvent | AuthUsersSearchSubmittedEvent | CommandMenuOpenedEvent + | CommandInputTypedEvent diff --git a/packages/ui-patterns/src/CommandMenu/api/CommandInput.tsx b/packages/ui-patterns/src/CommandMenu/api/CommandInput.tsx index 898fb92e3b9..b299dd7bf59 100644 --- a/packages/ui-patterns/src/CommandMenu/api/CommandInput.tsx +++ b/packages/ui-patterns/src/CommandMenu/api/CommandInput.tsx @@ -1,11 +1,13 @@ 'use client' -import React, { forwardRef, useEffect, useRef, useState } from 'react' +import { forwardRef, useCallback, useEffect, useRef, useState } from 'react' +import type React from 'react' import { useBreakpoint } from 'common' import { CommandInput_Shadcn_, cn } from 'ui' import { useQuery, useSetQuery } from './hooks/queryHooks' +import { useCommandMenuTelemetryContext } from './hooks/useCommandMenuTelemetryContext' function useFocusInputOnWiderScreens(ref: React.ForwardedRef) { const isBelowSm = useBreakpoint('sm') @@ -50,8 +52,74 @@ const CommandInput = forwardRef< const [inputValue, setInputValue] = useState(query) useEffect(() => { setInputValue(query) + previousValueRef.current = query }, [query]) + // Get telemetry context + const telemetryContext = useCommandMenuTelemetryContext() + + // Debounced telemetry tracking + const debounceTimeoutRef = useRef() + const previousValueRef = useRef(inputValue) + + const logTelemetryEvent = useCallback( + (value: string) => { + if (telemetryContext?.onTelemetry) { + // Create a CommandInputTypedEvent + const event = { + action: 'command_input_typed' as const, + properties: { + value: value, + app: telemetryContext.app, + }, + groups: {} as Record, // Groups will be populated by the telemetry system + } + // Cast to unknown first, then to the expected type to bypass type checking + // This is a workaround for the type mismatch between CommandInputTypedEvent and CommandMenuOpenedEvent + telemetryContext.onTelemetry( + event as unknown as Parameters[0] + ) + } + }, + [telemetryContext] + ) + + const debouncedLogTelemetry = useCallback( + (value: string) => { + // Only trigger telemetry if the user is adding characters (not removing with backspace) + const isAddingCharacters = value.length > previousValueRef.current.length + + if (!isAddingCharacters) { + previousValueRef.current = value + return + } + + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current) + } + + debounceTimeoutRef.current = setTimeout(() => { + logTelemetryEvent(value) + previousValueRef.current = value + }, 500) + }, + [logTelemetryEvent] + ) + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current) + } + } + }, []) + + const handleValueChange = (value: string) => { + setInputValue(value) + debouncedLogTelemetry(value) + } + // To handle CJK input const [imeComposing, setImeComposing] = useState(false) useEffect(() => { @@ -67,7 +135,7 @@ const CommandInput = forwardRef< autoFocus={false} ref={inputRef} value={inputValue} - onValueChange={setInputValue} + onValueChange={handleValueChange} placeholder="Run a command or search..." onCompositionStart={() => setImeComposing(true)} onCompositionEnd={() => setImeComposing(false)}