All files / app/components GlobalMessage.tsx

97.36% Statements 37/38
95% Branches 19/20
100% Functions 5/5
97.29% Lines 36/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83  3x 3x 3x 3x 3x       3x       30x 15x     15x 2x     13x   13x 3x     10x       32x   10x 6x     4x     29x 30x   30x   30x   30x 30x   30x 30x   8x       30x 20x 6x             30x 26x     4x   4x 2x       1x          
import { JSONValue } from "@growthbook/growthbook";
import { useFeatureValue } from "@growthbook/growthbook-react";
import { Alert as MuiAlert, AlertColor } from "@mui/material";
import Sentry from "platform/sentry";
import { usePersistedState } from "platform/usePersistedState";
import React, { useEffect } from "react";
 
type GlobalMessageData = { severity: AlertColor; message: string };
 
const SEVERITIES: readonly AlertColor[] = ["success", "info", "warning", "error"];
 
// { "severity": "info", "message": "Logins are <b>down</b>" }
function parseGlobalMessage(flag: JSONValue): GlobalMessageData | null {
  if (flag === null) {
    return null;
  }
 
  if (typeof flag !== "object" || Array.isArray(flag)) {
    throw new Error("global_message is not an object");
  }
 
  const { severity, message } = flag;
 
  if (message === undefined || message === "") {
    return null;
  }
 
  Iif (typeof message !== "string") {
    throw new Error("global_message.message is not a string");
  }
 
  const alertColor = SEVERITIES.find((s) => s === severity);
 
  if (alertColor === undefined) {
    throw new Error("global_message.severity is not a known severity");
  }
 
  return { severity: alertColor, message };
}
 
export function GlobalMessage() {
  const flag = useFeatureValue<JSONValue>("global_message", null);
  // dismissal is keyed on the banner's contents, so publishing anything different re-shows it
  const [dismissed, setDismissed] = usePersistedState<string | null>("globalmessage.dismissed", null);
 
  const flagJson = JSON.stringify(flag);
 
  let data: GlobalMessageData | null = null;
  let error: string | null = null;
 
  try {
    data = parseGlobalMessage(flag);
  } catch (e) {
    error = (e as Error).message;
  }
 
  // deps are strings because GrowthBook rebuilds the flag object on every refresh
  useEffect(() => {
    if (error !== null) {
      Sentry.captureException(new Error(error), {
        tags: { component: "GlobalMessage" },
        extra: { flag: flagJson },
      });
    }
  }, [error, flagJson]);
 
  if (data === null) {
    return null;
  }
 
  const key = `${data.severity}:${data.message}`;
 
  if (key === dismissed) {
    return null;
  }
 
  return (
    <MuiAlert severity={data.severity} onClose={() => setDismissed(key)}>
      <span dangerouslySetInnerHTML={{ __html: data.message }} />
    </MuiAlert>
  );
}