github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/AnalyticsNudge.tsx (about)

     1  import React, { Component } from "react"
     2  import "./AnalyticsNudge.scss"
     3  import { linkToTiltDocs, TiltDocsPage } from "./constants"
     4  
     5  export const NUDGE_TIMEOUT_MS = 15000
     6  const nudgeElem = (): JSX.Element => {
     7    return (
     8      <p>
     9        Welcome to Tilt! We collect anonymized usage data to help us improve. Is
    10        that OK? (
    11        <a
    12          href={linkToTiltDocs(TiltDocsPage.TelemetryFaq)}
    13          target="_blank"
    14          rel="noopener noreferrer"
    15        >
    16          Read more
    17        </a>
    18        )
    19      </p>
    20    )
    21  }
    22  const successOptInElem = (): JSX.Element => {
    23    return (
    24      <p data-testid="option-success">
    25        Thanks for helping us improve Tilt for you and everyone! (You can change
    26        your mind by running <code>tilt analytics opt out</code> in your
    27        terminal.)
    28      </p>
    29    )
    30  }
    31  const successOptOutElem = (): JSX.Element => {
    32    return (
    33      <p data-testid="optout-success">
    34        Okay, opting you out of telemetry. If you change your mind, you can run{" "}
    35        <code>tilt analytics opt in</code> in your terminal.
    36      </p>
    37    )
    38  }
    39  const errorElem = (respBody: string): JSX.Element => {
    40    return (
    41      <div role="alert">
    42        <p>Oh no, something went wrong! Request failed with:</p>
    43        <pre>{respBody}</pre>
    44        <p>
    45          <a
    46            href="https://tilt.dev/contact"
    47            target="_blank"
    48            rel="noopener noreferrer"
    49          >
    50            contact us
    51          </a>
    52        </p>
    53      </div>
    54    )
    55  }
    56  
    57  type AnalyticsNudgeProps = {
    58    needsNudge: boolean
    59  }
    60  
    61  type AnalyticsNudgeState = {
    62    requestMade: boolean
    63    optIn: boolean
    64    responseCode: number
    65    responseBody: string
    66    dismissed: boolean
    67    dismissTimeout: NodeJS.Timeout | null
    68  }
    69  
    70  class AnalyticsNudge extends Component<
    71    AnalyticsNudgeProps,
    72    AnalyticsNudgeState
    73  > {
    74    constructor(props: AnalyticsNudgeProps) {
    75      super(props)
    76  
    77      this.state = {
    78        requestMade: false,
    79        optIn: false,
    80        responseCode: 0,
    81        responseBody: "",
    82        dismissed: false,
    83        dismissTimeout: null,
    84      }
    85    }
    86  
    87    componentWillUnmount() {
    88      if (this.state.dismissTimeout !== null) {
    89        clearTimeout(this.state.dismissTimeout)
    90      }
    91    }
    92  
    93    shouldShow(): boolean {
    94      return (
    95        (this.props.needsNudge && !this.state.dismissed) ||
    96        (!this.state.dismissed && this.state.requestMade)
    97      )
    98    }
    99  
   100    analyticsOpt(optIn: boolean) {
   101      let url = `//${window.location.host}/api/analytics_opt`
   102  
   103      let payload = { opt: optIn ? "opt-in" : "opt-out" }
   104  
   105      this.setState({ requestMade: true, optIn: optIn })
   106  
   107      fetch(url, {
   108        method: "post",
   109        body: JSON.stringify(payload),
   110      }).then((response: Response) => {
   111        response.text().then((body: string) => {
   112          this.setState({
   113            responseCode: response.status,
   114            responseBody: body,
   115          })
   116        })
   117  
   118        if (response.status === 200) {
   119          // if we successfully recorded the choice, dismiss the nudge after a few seconds
   120          const dismissTimeout = setTimeout(() => {
   121            this.setState({ dismissed: true, dismissTimeout: null })
   122          }, NUDGE_TIMEOUT_MS)
   123  
   124          this.setState({ dismissTimeout })
   125        }
   126      })
   127    }
   128  
   129    dismiss() {
   130      this.setState({ dismissed: true })
   131    }
   132  
   133    messageElem(): JSX.Element {
   134      if (this.state.responseCode) {
   135        if (this.state.responseCode === 200) {
   136          // Successfully called opt endpt.
   137          if (this.state.optIn) {
   138            // User opted in
   139            return (
   140              <>
   141                {successOptInElem()}
   142                <span>
   143                  <button
   144                    className="AnalyticsNudge-button"
   145                    onClick={() => this.dismiss()}
   146                  >
   147                    Dismiss
   148                  </button>
   149                </span>
   150              </>
   151            )
   152          }
   153          // User opted out
   154          return (
   155            <>
   156              {successOptOutElem()}
   157              <span>
   158                <button
   159                  className="AnalyticsNudge-button"
   160                  onClick={() => this.dismiss()}
   161                >
   162                  Dismiss
   163                </button>
   164              </span>
   165            </>
   166          )
   167        } else {
   168          return (
   169            // Error calling the opt endpt.
   170            <>
   171              {errorElem(this.state.responseBody)}
   172              <span>
   173                <button
   174                  className="AnalyticsNudge-button"
   175                  onClick={() => this.dismiss()}
   176                >
   177                  Dismiss
   178                </button>
   179              </span>
   180            </>
   181          )
   182        }
   183      }
   184  
   185      if (this.state.requestMade) {
   186        // Request in progress
   187        return <p data-testid="opt-loading">Okay, making it so...</p>
   188      }
   189      return (
   190        <>
   191          {nudgeElem()}
   192          <span>
   193            <button
   194              className="AnalyticsNudge-button"
   195              onClick={() => this.analyticsOpt(false)}
   196            >
   197              Nope
   198            </button>
   199            <button
   200              className="AnalyticsNudge-button opt-in"
   201              onClick={() => this.analyticsOpt(true)}
   202            >
   203              I'm in
   204            </button>
   205          </span>
   206        </>
   207      )
   208    }
   209    render() {
   210      if (!this.shouldShow()) {
   211        return null
   212      }
   213  
   214      return (
   215        <aside
   216          aria-label="Tilt analytics options"
   217          className="AnalyticsNudge is-visible"
   218        >
   219          {this.messageElem()}
   220        </aside>
   221      )
   222    }
   223  }
   224  
   225  export default AnalyticsNudge