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  }