/ shared / logger / src / local-storage-filter.ts
local-storage-filter.ts
  1  export type Level = 'debug' | 'info' | 'warn' | 'error';
  2  // Numbers correspond to the levels above, with 0 meaning "no level"
  3  type LevelNum = 4 | 3 | 2 | 1 | 0;
  4  
  5  interface Rules {
  6      named?: Record<string, LevelNum>;
  7      defaultLevel?: LevelNum;
  8  }
  9  
 10  const LEVEL_TO_NUM: Record<Level | 'off' | '*' | '', LevelNum> = {
 11      '*': 4,
 12      debug: 4,
 13      info: 3,
 14      warn: 2,
 15      error: 1,
 16      off: 0,
 17      '': 0,
 18  };
 19  
 20  /**
 21   * Parses log filtering instructions from localStorage.onyxLog.
 22   * The instructions are a series of comma separated directives that restrict
 23   * logging. Restrictions indicate the highest log level that a named logger
 24   * will emit. The name of the logger is the string passed to
 25   * LoggerFactory.loggerFor.
 26   *
 27   * By default (ex. empty rule string), no logs will be emitted.
 28   *
 29   * The format of the directives is NAME=LEVEL. LEVEL can be one of:
 30   *
 31   *   - * - all levels are logged (debug, info, warn, error)
 32   *   - debug - same as above
 33   *   - info - everything but debug is logged
 34   *   - warn - everything but info and debug is logged
 35   *   - error - only errors are logged
 36   *   - off (or empty string, ex. "MyClass=") - nothing will be logged
 37   *
 38   * Some examples:
 39   *
 40   *   - '*=*' will emit all log levels from all loggers
 41   *   - '*=info,Foo=off' will emit everything but debug except or logs from
 42   *     the named logger Foo (which will be entirely suppressed)
 43   *   - 'Bar=error,Baz=warn' will emit errors from Bar and Baz and warnings from
 44   *     Baz
 45   *
 46   * NOTE: Keep this in sync with README.md!
 47   */
 48  function parseRules(): Rules {
 49      const onyxLog: string = (() => {
 50          try {
 51              // The typeof check is for SSR
 52              return (
 53                  (typeof window !== 'undefined'
 54                      ? window.localStorage.onyxLog
 55                      : '') || ''
 56              );
 57          } catch {
 58              // window.localStorage will throw when referenced (at all) when
 59              // Chrome has it disabled
 60              // See: rdar://93367396 (Guard localStorage and sessionStorage use)
 61              return '';
 62          }
 63      })();
 64  
 65      const PRODUCTION_DEFAULT = {}; // no logs unless specified
 66      const DEV_DEFAULT = {
 67          defaultLevel: LEVEL_TO_NUM['*'], // All logs unless specified
 68      };
 69      const isDevelopment = (() => {
 70          // This is a little tricky. The ENV var is not real. It's replaced by
 71          // rollup-plugin-replace. Thus, we can't do the usual of testing for
 72          // the existence of `process` and then doing `process?.env` etc.
 73          // Instead, we just try the whole thing and try/catch. This way,
 74          // rollup-plugin-replace sees that entire string verbatim and can
 75          // replace it with the proper environment.
 76          try {
 77              // @ts-ignore
 78              return process.env.NODE_ENV !== 'production';
 79          } catch {
 80              return false;
 81          }
 82      })();
 83      const defaultRules = isDevelopment ? DEV_DEFAULT : PRODUCTION_DEFAULT;
 84  
 85      // If the localStorage is specified, start from a clean slate. Otherwise,
 86      // use the environment default
 87      const rules: Rules = onyxLog.length > 0 ? {} : defaultRules;
 88  
 89      for (const directive of onyxLog.split(',').filter((v) => v)) {
 90          // Invalid directive, must be of the form 'name=level'
 91          const parts = directive.split('=');
 92          if (parts.length !== 2) {
 93              continue;
 94          }
 95  
 96          const [name, maxLevelName] = parts;
 97          const maxLevel =
 98              LEVEL_TO_NUM[maxLevelName as keyof typeof LEVEL_TO_NUM];
 99  
100          // Invalid level
101          if (typeof maxLevel === 'undefined') {
102              continue;
103          }
104  
105          if (name === '*') {
106              rules.defaultLevel = maxLevel;
107          } else {
108              rules.named = rules.named ?? {};
109              rules.named[name] = maxLevel;
110          }
111      }
112  
113      return rules;
114  }
115  
116  export function shouldLog(name: string, level: Level): boolean {
117      const rules = parseRules();
118  
119      // Rules for the named logger take precedence over the default
120      const maxLevel = (rules.named || {})[name] ?? rules.defaultLevel ?? 0;
121      return LEVEL_TO_NUM[level] <= maxLevel;
122  }