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)}