github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/instrumentedComponents.tsx (about) 1 import { 2 Button, 3 ButtonProps, 4 Checkbox, 5 CheckboxProps, 6 debounce, 7 TextField, 8 TextFieldProps, 9 } from "@material-ui/core" 10 import React, { useMemo } from "react" 11 import { AnalyticsAction, incr, Tags } from "./analytics" 12 13 // Shared components that implement analytics 14 // 1. Saves callers from having to implement/test analytics for every interactive 15 // component. 16 // 2. Allows wrappers to cheaply require analytics params. 17 18 type InstrumentationProps = { 19 analyticsName: string 20 analyticsTags?: Tags 21 } 22 23 export function InstrumentedButton(props: ButtonProps & InstrumentationProps) { 24 const { analyticsName, analyticsTags, onClick, ...buttonProps } = props 25 const instrumentedOnClick: typeof onClick = (e) => { 26 incr(analyticsName, { 27 action: AnalyticsAction.Click, 28 ...(analyticsTags ?? {}), 29 }) 30 if (onClick) { 31 onClick(e) 32 } 33 } 34 35 // TODO(nick): variant="outline" doesn't seem like the right default. 36 return ( 37 <Button 38 variant="outlined" 39 disableRipple={true} 40 onClick={instrumentedOnClick} 41 {...buttonProps} 42 /> 43 ) 44 } 45 46 // How long to debounce TextField edit events. i.e., only send one edit 47 // event per this duration. These don't need to be submitted super 48 // urgently, and we want to be closer to sending one per user intent than 49 // one per keystroke. 50 export const textFieldEditDebounceMilliseconds = 5000 51 52 export function InstrumentedTextField( 53 props: TextFieldProps & InstrumentationProps 54 ) { 55 const { analyticsName, analyticsTags, onChange, ...textFieldProps } = props 56 57 // we have to memoize the debounced function so that incrs reuse the same debounce timer 58 const debouncedIncr = useMemo( 59 () => 60 // debounce so we don't send analytics for every single keypress 61 debounce((name: string, tags?: Tags) => { 62 incr(name, { 63 action: AnalyticsAction.Edit, 64 ...(tags ?? {}), 65 }) 66 }, textFieldEditDebounceMilliseconds), 67 [] 68 ) 69 70 const instrumentedOnChange: typeof onChange = (e) => { 71 debouncedIncr(analyticsName, analyticsTags) 72 if (onChange) { 73 onChange(e) 74 } 75 } 76 77 return <TextField onChange={instrumentedOnChange} {...textFieldProps} /> 78 } 79 80 export function InstrumentedCheckbox( 81 props: CheckboxProps & InstrumentationProps 82 ) { 83 const { analyticsName, analyticsTags, onChange, ...checkboxProps } = props 84 const instrumentedOnChange: typeof onChange = (e, checked) => { 85 incr(analyticsName, { 86 action: AnalyticsAction.Edit, 87 ...(analyticsTags ?? {}), 88 }) 89 if (onChange) { 90 onChange(e, checked) 91 } 92 } 93 94 return ( 95 <Checkbox 96 onChange={instrumentedOnChange} 97 disableRipple={true} 98 {...checkboxProps} 99 /> 100 ) 101 }