All files logger.js

100% Statements 112/112
98.11% Branches 52/53
100% Functions 15/15
100% Lines 112/112

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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 1139x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x 398x 398x 9x 9x 445x 444x 445x 9x 9x 9x 122x 241x 122x 122x 122x 9x 119x 119x 119x 119x 1x 1x 119x 9x 67x 67x 67x 67x 67x 66x 67x 9x 9x 9x 9x 9x 9x 9x 9x 340x 340x 340x 340x 9x 9x 1294x 1294x 9x 9x 455x 455x 455x 455x 9x 9x 1294x 455x 455x 889x 1x 1x 1294x 9x 9x 18x 18x 9x 9x 102x 102x 9x 9x 114x 114x 9x 9x 1060x 1060x 9x 9x 9x 9x 9x 333x 333x 333x 9x 9x 876x 876x 19x 19x 19x 876x 1x 1x 876x 9x  
// Logging with levels and redaction (DESIGN 2.7). secretKey, signature, token and audio bytes are
// never logged; appKey is logged as its first 4 characters plus ***.
 
export const LogLevel = Object.freeze({
  OFF: 'OFF',
  ERROR: 'ERROR',
  WARN: 'WARN',
  INFO: 'INFO',
  DEBUG: 'DEBUG',
});
 
const RANK = { OFF: 0, ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4 };
 
export function isLogLevel(v) {
  return typeof v === 'string' && Object.prototype.hasOwnProperty.call(RANK, v);
}
 
export function maskAppKey(appKey) {
  if (!appKey) return '';
  return String(appKey).slice(0, 4) + '***';
}
 
/** Redacts credential query parameters of a URL before it is logged. */
export function redactUrl(url) {
  return String(url).replace(/([?&])(signature|token|secretKey|appKey)=([^&#]*)/g, (all, sep, name, value) => {
    if (name === 'appKey') return sep + name + '=' + maskAppKey(decodeSafe(value));
    return sep + name + '=***';
  });
}
 
function decodeSafe(v) {
  try {
    return decodeURIComponent(v);
  } catch (e) {
    return v;
  }
}
 
function consoleSink(level, tag, message, error) {
  if (typeof console === 'undefined') return;
  const fn = level === 'ERROR' ? console.error : level === 'WARN' ? console.warn : level === 'INFO' ? console.info : console.log;
  const line = '[' + tag + '] ' + message;
  if (error !== undefined) fn.call(console, line, error);
  else fn.call(console, line);
}
 
export class Logger {
  /**
   * @param level   OFF, ERROR, WARN, INFO or DEBUG
   * @param sink    function (level, tag, message, error) or undefined for the console
   * @param secrets strings that must never appear in a log line (secretKey, token)
   */
  constructor(level, sink, secrets) {
    this.level = isLogLevel(level) ? level : LogLevel.WARN;
    this.sink = typeof sink === 'function' ? sink : consoleSink;
    this.secrets = (secrets || []).filter((s) => typeof s === 'string' && s.length >= 4);
  }
 
  enabled(level) {
    return RANK[level] > 0 && RANK[level] <= RANK[this.level];
  }
 
  scrub(text) {
    let s = String(text);
    for (let i = 0; i < this.secrets.length; i++) s = s.split(this.secrets[i]).join('***');
    return s;
  }
 
  log(level, tag, message, error) {
    if (!this.enabled(level)) return;
    try {
      this.sink(level, tag, this.scrub(message), error);
    } catch (e) {
      // A failing sink must never break the SDK.
    }
  }
 
  error(tag, message, error) {
    this.log('ERROR', tag, message, error);
  }
 
  warn(tag, message, error) {
    this.log('WARN', tag, message, error);
  }
 
  info(tag, message, error) {
    this.log('INFO', tag, message, error);
  }
 
  debug(tag, message, error) {
    this.log('DEBUG', tag, message, error);
  }
}
 
/** Calls optional EventListener hooks (DESIGN 2.7), isolating the SDK from exceptions they throw. */
export class EventHub {
  constructor(listener, logger) {
    this.listener = listener && typeof listener === 'object' ? listener : null;
    this.logger = logger;
  }
 
  emit(name) {
    const l = this.listener;
    if (!l || typeof l[name] !== 'function') return;
    const args = Array.prototype.slice.call(arguments, 1);
    try {
      l[name].apply(l, args);
    } catch (e) {
      if (this.logger) this.logger.error('YuguSDK', 'eventListener.' + name + ' threw', e);
    }
  }
}