GregBrimble/cf-workers-dashboard

View on GitHub
packages/client/src/components/workers/Analytics.tsx

Summary

Maintainability
F
4 days
Test Coverage
import React, { useState } from "react";
import Plot from "react-plotly.js";
import { BigStatus } from "../BigStatus";
import { useQuery, gql } from "@apollo/client";
import { useParams } from "react-router-dom";
import { LazyRender } from "../LazyRender";

const DEFAULT_HIDDEN_SERIES = ["p25", "p75", "p99", "p999"];

const SEVEN_DAYS = 1000 * 60 * 60 * 24 * 7;

const analyticsToSeries = (analytics: any, interval: string) => {
  const min = {
    x: [] as Date[],
    y: [] as number[],
    type: "scatter",
    name: "min",
    mode: "lines+markers",
    line: { shape: "spline", smoothing: 1.3 },
  };
  const p25 = {
    x: [] as Date[],
    y: [] as number[],
    type: "scatter",
    name: "p25",
    mode: "lines+markers",
    line: { shape: "spline", smoothing: 1.3 },
  };
  const p50 = {
    x: [] as Date[],
    y: [] as number[],
    type: "scatter",
    name: "p50",
    mode: "lines+markers",
    line: { shape: "spline", smoothing: 1.3 },
  };
  const p75 = {
    x: [] as Date[],
    y: [] as number[],
    type: "scatter",
    name: "p75",
    mode: "lines+markers",
    line: { shape: "spline", smoothing: 1.3 },
  };
  const p90 = {
    x: [] as Date[],
    y: [] as number[],
    type: "scatter",
    name: "p90",
    mode: "lines+markers",
    line: { shape: "spline", smoothing: 1.3 },
  };
  const p99 = {
    x: [] as Date[],
    y: [] as number[],
    type: "scatter",
    name: "p99",
    mode: "lines+markers",
    line: { shape: "spline", smoothing: 1.3 },
  };
  const p999 = {
    x: [] as Date[],
    y: [] as number[],
    type: "scatter",
    name: "p999",
    mode: "lines+markers",
    line: { shape: "spline", smoothing: 1.3 },
  };
  const max = {
    x: [] as Date[],
    y: [] as number[],
    type: "scatter",
    name: "max",
    mode: "lines+markers",
    line: { shape: "spline", smoothing: 1.3 },
  };
  const errors = {
    x: [] as Date[],
    y: [] as number[],
    type: "bar",
    yaxis: "y2",
    name: "errors",
    marker: {
      opacity: "0.4",
    },
  };
  const requests = {
    x: [] as Date[],
    y: [] as number[],
    type: "bar",
    yaxis: "y2",
    name: "requests",
    marker: {
      opacity: "0.4",
    },
  };
  const subrequests = {
    x: [] as Date[],
    y: [] as number[],
    type: "bar",
    yaxis: "y2",
    name: "subrequests",
    marker: {
      opacity: "0.4",
    },
  };
  const series = [
    min,
    p25,
    p50,
    p75,
    p90,
    p99,
    p999,
    max,
    errors,
    requests,
    subrequests,
  ];

  for (const analytic of analytics.sort(
    (a: any, b: any) =>
      new Date(a.dimensions[interval]).getTime() -
      new Date(b.dimensions[interval]).getTime()
  )) {
    min.y.push(analytic.min.cpuTime / 1000);
    p25.y.push(analytic.quantiles.cpuTimeP25 / 1000);
    p50.y.push(analytic.quantiles.cpuTimeP50 / 1000);
    p75.y.push(analytic.quantiles.cpuTimeP75 / 1000);
    p90.y.push(analytic.quantiles.cpuTimeP90 / 1000);
    p99.y.push(analytic.quantiles.cpuTimeP99 / 1000);
    p999.y.push(analytic.quantiles.cpuTimeP999 / 1000);
    max.y.push(analytic.max.cpuTime / 1000);
    errors.y.push(analytic.sum.errors);
    requests.y.push(analytic.sum.requests);
    subrequests.y.push(analytic.sum.subrequests);

    series.map((series) => series.x.push(analytic.dimensions[interval]));
  }

  for (const serie of series) {
    if (DEFAULT_HIDDEN_SERIES.includes(serie.name)) {
      (serie as any).visible = "legendonly";
    }
  }

  return series;
};

const maxTime = (analytics: any[]) =>
  Math.ceil(
    Math.max(
      ...analytics.map((analytic: any) => analytic.max.cpuTime / 1000),
      5
    )
  );

export const Analytics = () => {
  const { workerID: scriptID, accountID } = useParams();

  const now = new Date();
  const defaultDateFrom = new Date(now);
  defaultDateFrom.setDate(now.getDate() - 7);
  const [dateFrom, setDateFrom] = useState(defaultDateFrom);
  const [dateTo, setDateTo] = useState(now);
  const [interval, setInterval] = useState("date");
  const [hoveredInterval, setHoveredInterval] = useState(now);

  const { loading, error, data } = useQuery(
    gql`
  query($accountID: ID!, $scriptID: ID!, $filter: AnalyticsFilterInput!) {
    account(id: $accountID) {
      script(id: $scriptID) {
        analytics(filter: $filter, limit: 10000) {
          dimensions {
            ${interval}
          }
          min {
            cpuTime
          }
          quantiles {
            cpuTimeP25
            cpuTimeP50
            cpuTimeP75
            cpuTimeP90
            cpuTimeP99
            cpuTimeP999
          }
          max {
            cpuTime
          }
          sum {
            errors
            requests
            subrequests
          }
        }
      }
    }
  }
`,
    {
      variables: {
        accountID,
        scriptID,
        filter: {
          date_geq: dateFrom.toISOString().split("T")[0],
          date_lt: dateTo.toISOString().split("T")[0],
        },
      },
    }
  );

  const {
    loading: loadingCPUTime,
    error: errorCPUTime,
    data: dataCPUTime,
  } = useQuery(
    gql`
      query($accountID: ID!, $scriptID: ID!, $filter: AnalyticsFilterInput!) {
        account(id: $accountID) {
          script(id: $scriptID) {
            analytics(filter: $filter, limit: 10000) {
              dimensions {
                status
              }
              min {
                cpuTime
              }
              quantiles {
                cpuTimeP25
                cpuTimeP50
                cpuTimeP75
                cpuTimeP90
                cpuTimeP99
                cpuTimeP999
              }
              max {
                cpuTime
              }
            }
          }
        }
      }
    `,
    {
      variables: {
        accountID,
        scriptID,
        filter: {
          date: hoveredInterval.toISOString().split("T")[0],
        },
      },
    }
  );

  return (
    <div className="w-full">
      <div className="px-12 pt-4 pb-8 bg-gray-50">
        <div className="grid grid-cols-1 col-gap-4 sm:grid-cols-6">
          <div className="sm:col-span-3">
            <label
              htmlFor="dateFrom"
              className="block text-sm font-medium leading-5 text-gray-700"
            >
              From
            </label>
            <div className="mt-1 rounded-md shadow-sm">
              <input
                id="dateFrom"
                className="form-input block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"
                type="date"
                value={dateFrom.toISOString().split("T")[0]}
                onChange={(event) => {
                  if (event.target.value) {
                    const duration =
                      dateTo.getTime() - new Date(event.target.value).getTime();
                    if (duration < SEVEN_DAYS && duration > 0)
                      setDateFrom(new Date(event.target.value));
                  }
                }}
                required={true}
              />
            </div>
          </div>
          <div className="mt-2 sm:mt-0 sm:col-span-3">
            <label
              htmlFor="dateTo"
              className="block text-sm font-medium leading-5 text-gray-700"
            >
              To
            </label>
            <div className="mt-1 rounded-md shadow-sm">
              <input
                id="dateTo"
                className="form-input block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"
                type="date"
                value={dateTo.toISOString().split("T")[0]}
                onChange={(event) => {
                  if (event.target.value) {
                    const duration =
                      new Date(event.target.value).getTime() -
                      dateFrom.getTime();
                    if (duration < SEVEN_DAYS && duration > 0)
                      setDateTo(new Date(event.target.value));
                  }
                }}
                required={true}
              />
            </div>
          </div>
          <div className="mt-2 sm:col-span-6">
            <label
              htmlFor="interval"
              className="block text-sm font-medium leading-5 text-gray-700"
            >
              Interval
            </label>
            <div className="mt-1 rounded-md shadow-sm">
              <select
                id="interval"
                className="block form-select w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"
                value={interval}
                onChange={(event) => {
                  if (event.target.value) {
                    setInterval(event.target.value);
                  }
                }}
                required={true}
              >
                <option value="date">Date</option>
                <option value="datetimeHour">Date {"&"} Hour</option>
                <option value="datetime">Timestamp</option>
              </select>
            </div>
          </div>
        </div>
      </div>
      <LazyRender
        loading={loading}
        error={error}
        data={data}
        render={(data) => {
          const { analytics } = data.account.script;

          const series = analyticsToSeries(
            JSON.parse(JSON.stringify(analytics)),
            interval
          ) as any;
          return (
            <Plot
              data={series}
              layout={{
                title: "CPU Time, Errors, Requests and Subrequests over Time",
                yaxis: {
                  title: "CPU Time (ms)",
                  range: [0, maxTime(analytics)],
                },
                yaxis2: {
                  title: "Count",
                  overlaying: "y",
                  side: "right",
                },
                barmode: "group",
                legend: { orientation: "h" },
              }}
              config={{ responsive: true }}
              style={{ width: "100%", height: 500 }}
              onClick={(click) => {
                if (click.points[0].x)
                  setHoveredInterval(new Date(click.points[0].x));
              }}
            />
          );
        }}
        name="Analytics"
      />
      <div className="px-12 pt-4 pb-8 bg-gray-50">
        <div className="grid grid-cols-1 col-gap-4 sm:grid-cols-6">
          <div className="sm:col-span-6">
            <label
              htmlFor="interval"
              className="block text-sm font-medium leading-5 text-gray-700"
            >
              Date
            </label>
            <div className="mt-1 rounded-md shadow-sm">
              <input
                id="dateTo"
                className="form-input block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"
                type="date"
                value={hoveredInterval.toISOString().split("T")[0]}
                onChange={(event) => {
                  if (event.target.value) {
                    setHoveredInterval(new Date(event.target.value));
                  }
                }}
                required={true}
              />
            </div>
          </div>
        </div>
      </div>
      <LazyRender
        loading={loadingCPUTime}
        error={errorCPUTime}
        data={dataCPUTime}
        render={(data) => {
          const { analytics } = data.account.script;

          const series = analytics.map((analytic: any) => ({
            x: [0, 0.25, 0.5, 0.75, 0.9, 0.99, 0.999, 1],
            y: [
              analytic.min.cpuTime / 1000,
              analytic.quantiles.cpuTimeP25 / 1000,
              analytic.quantiles.cpuTimeP50 / 1000,
              analytic.quantiles.cpuTimeP75 / 1000,
              analytic.quantiles.cpuTimeP90 / 1000,
              analytic.quantiles.cpuTimeP99 / 1000,
              analytic.quantiles.cpuTimeP999 / 1000,
              analytic.max.cpuTime / 1000,
            ],
            type: "scatter",
            name: analytic.dimensions.status,
            mode: "lines+markers",
            line: { shape: "spline", smoothing: 1.3 },
          })) as any;

          return (
            <Plot
              data={series}
              layout={{
                title: `CPU Time by Status on ${
                  hoveredInterval.toISOString().split("T")[0]
                }`,
                yaxis: {
                  title: "CPU Time (ms)",
                  range: [0, maxTime(analytics)],
                },
                xaxis: { title: "Quantile" },
                showlegend: true,
              }}
              config={{ responsive: true }}
              style={{ width: "100%", height: 500 }}
            />
          );
        }}
        name="CPU Time"
      />
    </div>
  );
};