• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

uber-web / probe.gl / 3732260095

pending completion
3732260095

push

github

GitHub
chore: stricter typescript settings (#213)

262 of 614 branches covered (42.67%)

Branch coverage included in aggregate %.

29 of 29 new or added lines in 6 files covered. (100.0%)

549 of 969 relevant lines covered (56.66%)

283546.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.04
/modules/log/src/log.ts
1
// probe.gl, MIT license
2

3
/* eslint-disable no-console */
4
import {VERSION, isBrowser} from '@probe.gl/env';
5
import {LocalStorage} from './utils/local-storage';
6
import {formatImage, formatTime, leftPad} from './utils/formatters';
7
import {addColor} from './utils/color';
8
import {autobind} from './utils/autobind';
9
import assert from './utils/assert';
10
import {getHiResTimestamp} from './utils/hi-res-timestamp';
11
import * as asciify from './node/node-asciify-image';
12

13
// Instrumentation in other packages may override console methods, so preserve them here
14
const originalConsole = {
1✔
15
  debug: isBrowser ? console.debug || console.log : console.log,
2!
16
  log: console.log,
17
  info: console.info,
18
  warn: console.warn,
19
  error: console.error
20
};
21

22
type Table = Record<string, any>;
23

24
type LogFunction = () => void;
25

26
type LogOptions = {
27
  method?;
28
  time?;
29
  total?: number;
30
  delta?: number;
31
  tag?: string;
32
  message?: string;
33
  once?: boolean;
34
  nothrottle?: boolean;
35
  args?: any;
36
};
37

38
type LogSettings = {
39
  enabled?: boolean;
40
  level?: number;
41
  [key: string]: any;
42
};
43

44
const DEFAULT_SETTINGS: Required<LogSettings> = {
1✔
45
  enabled: true,
46
  level: 0
47
};
48

49
function noop() {} // eslint-disable-line @typescript-eslint/no-empty-function
50

51
const cache = {};
1✔
52
const ONCE = {once: true};
1✔
53

54
type LogConfiguration = {
55
  enabled?: boolean;
56
  level?: number;
57
};
58

59
/** A console wrapper */
60

61
export class Log {
62
  static VERSION = VERSION;
1✔
63

64
  id: string;
65
  VERSION: string = VERSION;
25✔
66
  _startTs: number = getHiResTimestamp();
25✔
67
  _deltaTs: number = getHiResTimestamp();
25✔
68
  _storage: LocalStorage<LogConfiguration>;
69
  userData = {};
25✔
70

71
  // TODO - fix support from throttling groups
72
  LOG_THROTTLE_TIMEOUT: number = 0; // Time before throttled messages are logged again
25✔
73

74
  constructor({id} = {id: ''}) {
×
75
    this.id = id;
25✔
76
    this.userData = {};
25✔
77
    this._storage = new LocalStorage<LogConfiguration>(`__probe-${this.id}__`, DEFAULT_SETTINGS);
25✔
78

79
    this.timeStamp(`${this.id} started`);
25✔
80

81
    autobind(this);
25✔
82
    Object.seal(this);
25✔
83
  }
84

85
  set level(newLevel: number) {
86
    this.setLevel(newLevel);
×
87
  }
88

89
  get level(): number {
90
    return this.getLevel();
50✔
91
  }
92

93
  isEnabled(): boolean {
94
    return this._storage.config.enabled;
59✔
95
  }
96

97
  getLevel(): number {
98
    return this._storage.config.level;
109✔
99
  }
100

101
  /** @return milliseconds, with fractions */
102
  getTotal(): number {
103
    return Number((getHiResTimestamp() - this._startTs).toPrecision(10));
62✔
104
  }
105

106
  /** @return milliseconds, with fractions */
107
  getDelta(): number {
108
    return Number((getHiResTimestamp() - this._deltaTs).toPrecision(10));
58✔
109
  }
110

111
  /** @deprecated use logLevel */
112
  set priority(newPriority: number) {
113
    this.level = newPriority;
×
114
  }
115

116
  /** @deprecated use logLevel */
117
  get priority(): number {
118
    return this.level;
25✔
119
  }
120

121
  /** @deprecated use logLevel */
122
  getPriority(): number {
123
    return this.level;
×
124
  }
125

126
  // Configure
127

128
  enable(enabled: boolean = true): this {
×
129
    this._storage.setConfiguration({enabled});
×
130
    return this;
×
131
  }
132

133
  setLevel(level: number): this {
134
    this._storage.setConfiguration({level});
×
135
    return this;
×
136
  }
137

138
  /** return the current status of the setting */
139
  get(setting: string): any {
140
    return this._storage.config[setting];
2✔
141
  }
142

143
  // update the status of the setting
144
  set(setting: string, value: any): void {
145
    this._storage.setConfiguration({[setting]: value});
2✔
146
  }
147

148
  /** Logs the current settings as a table */
149
  settings(): void {
150
    if (console.table) {
2!
151
      console.table(this._storage.config);
2✔
152
    } else {
153
      console.log(this._storage.config);
×
154
    }
155
  }
156

157
  // Unconditional logging
158

159
  assert(condition: unknown, message?: string): asserts condition {
160
    assert(condition, message);
4✔
161
  }
162

163
  /** Warn, but only once, no console flooding */
164
  warn(message: string, ...args): LogFunction;
165
  warn(message: string): LogFunction {
166
    return this._getLogFunction(0, message, originalConsole.warn, arguments, ONCE);
2✔
167
  }
168

169
  /** Print an error */
170
  error(message: string, ...args): LogFunction;
171
  error(message: string): LogFunction {
172
    return this._getLogFunction(0, message, originalConsole.error, arguments);
2✔
173
  }
174

175
  /** Print a deprecation warning */
176
  deprecated(oldUsage: string, newUsage: string): LogFunction {
177
    return this.warn(`\`${oldUsage}\` is deprecated and will be removed \
×
178
in a later version. Use \`${newUsage}\` instead`);
179
  }
180

181
  /** Print a removal warning */
182
  removed(oldUsage: string, newUsage: string): LogFunction {
183
    return this.error(`\`${oldUsage}\` has been removed. Use \`${newUsage}\` instead`);
×
184
  }
185

186
  // Conditional logging
187

188
  /** Log to a group */
189
  probe(logLevel, message?, ...args): LogFunction;
190
  probe(logLevel, message?): LogFunction {
191
    return this._getLogFunction(logLevel, message, originalConsole.log, arguments, {
4✔
192
      time: true,
193
      once: true
194
    });
195
  }
196

197
  /** Log a debug message */
198
  log(logLevel, message?, ...args): LogFunction;
199
  log(logLevel, message?): LogFunction {
200
    return this._getLogFunction(logLevel, message, originalConsole.debug, arguments);
14✔
201
  }
202

203
  /** Log a normal message */
204
  info(logLevel, message?, ...args): LogFunction;
205
  info(logLevel, message?): LogFunction {
206
    return this._getLogFunction(logLevel, message, console.info, arguments);
×
207
  }
208

209
  /** Log a normal message, but only once, no console flooding */
210
  once(logLevel, message?, ...args): LogFunction;
211
  once(logLevel, message?, ...args) {
212
    return this._getLogFunction(
4✔
213
      logLevel,
214
      message,
215
      originalConsole.debug || originalConsole.info,
4!
216
      arguments,
217
      ONCE
218
    );
219
  }
220

221
  /** Logs an object as a table */
222
  table(logLevel, table?, columns?): LogFunction {
223
    if (table) {
4!
224
      // @ts-expect-error Not clear how this works, columns being passed as arguments
225
      return this._getLogFunction(logLevel, table, console.table || noop, columns && [columns], {
4!
226
        tag: getTableHeader(table)
227
      });
228
    }
229
    return noop;
×
230
  }
231

232
  /** logs an image under Chrome */
233
  image({logLevel, priority, image, message = '', scale = 1}): LogFunction {
×
234
    if (!this._shouldLog(logLevel || priority)) {
×
235
      return noop;
×
236
    }
237
    return isBrowser
×
238
      ? logImageInBrowser({image, message, scale})
239
      : logImageInNode({image, message, scale});
240
  }
241

242
  time(logLevel, message) {
243
    return this._getLogFunction(logLevel, message, console.time ? console.time : console.info);
×
244
  }
245

246
  timeEnd(logLevel, message) {
247
    return this._getLogFunction(
×
248
      logLevel,
249
      message,
250
      console.timeEnd ? console.timeEnd : console.info
×
251
    );
252
  }
253

254
  timeStamp(logLevel, message?) {
255
    return this._getLogFunction(logLevel, message, console.timeStamp || noop);
25!
256
  }
257

258
  group(logLevel, message, opts = {collapsed: false}) {
2✔
259
    const options = normalizeArguments({logLevel, message, opts});
2✔
260
    const {collapsed} = opts;
2✔
261
    // @ts-expect-error
262
    options.method = (collapsed ? console.groupCollapsed : console.group) || console.info;
2!
263

264
    return this._getLogFunction(options);
2✔
265
  }
266

267
  groupCollapsed(logLevel, message, opts = {}) {
×
268
    return this.group(logLevel, message, Object.assign({}, opts, {collapsed: true}));
×
269
  }
270

271
  groupEnd(logLevel) {
272
    return this._getLogFunction(logLevel, '', console.groupEnd || noop);
2!
273
  }
274

275
  // EXPERIMENTAL
276

277
  withGroup(logLevel: number, message: string, func: Function): void {
278
    this.group(logLevel, message)();
×
279

280
    try {
×
281
      func();
×
282
    } finally {
283
      this.groupEnd(logLevel)();
×
284
    }
285
  }
286

287
  trace(): void {
288
    if (console.trace) {
×
289
      console.trace();
×
290
    }
291
  }
292

293
  // PRIVATE METHODS
294

295
  /** Deduces log level from a variety of arguments */
296
  _shouldLog(logLevel: unknown): boolean {
297
    return this.isEnabled() && this.getLevel() >= normalizeLogLevel(logLevel);
59✔
298
  }
299

300
  _getLogFunction(
301
    logLevel: unknown,
302
    message?: unknown,
303
    method?: Function,
304
    args?: IArguments,
305
    opts?: LogOptions
306
  ): LogFunction {
307
    if (this._shouldLog(logLevel)) {
59✔
308
      // normalized opts + timings
309
      opts = normalizeArguments({logLevel, message, args, opts});
58✔
310
      method = method || opts.method;
58✔
311
      assert(method);
58✔
312

313
      opts.total = this.getTotal();
58✔
314
      opts.delta = this.getDelta();
58✔
315
      // reset delta timer
316
      this._deltaTs = getHiResTimestamp();
58✔
317

318
      const tag = opts.tag || opts.message;
58✔
319

320
      if (opts.once) {
58✔
321
        if (!cache[tag]) {
10✔
322
          cache[tag] = getHiResTimestamp();
1✔
323
        } else {
324
          return noop;
9✔
325
        }
326
      }
327

328
      // TODO - Make throttling work with groups
329
      // if (opts.nothrottle || !throttle(tag, this.LOG_THROTTLE_TIMEOUT)) {
330
      //   return noop;
331
      // }
332

333
      message = decorateMessage(this.id, opts.message, opts);
49✔
334

335
      // Bind console function so that it can be called after being returned
336
      return method.bind(console, message, ...opts.args);
49✔
337
    }
338
    return noop;
1✔
339
  }
340
}
341

342
/**
343
 * Get logLevel from first argument:
344
 * - log(logLevel, message, args) => logLevel
345
 * - log(message, args) => 0
346
 * - log({logLevel, ...}, message, args) => logLevel
347
 * - log({logLevel, message, args}) => logLevel
348
 */
349
function normalizeLogLevel(logLevel: unknown): number {
350
  if (!logLevel) {
123✔
351
    return 0;
36✔
352
  }
353
  let resolvedLevel;
354

355
  switch (typeof logLevel) {
87✔
356
    case 'number':
357
      resolvedLevel = logLevel;
4✔
358
      break;
4✔
359

360
    case 'object':
361
      // Backward compatibility
362
      // TODO - deprecate `priority`
363
      // @ts-expect-error
364
      resolvedLevel = logLevel.logLevel || logLevel.priority || 0;
6✔
365
      break;
6✔
366

367
    default:
368
      return 0;
77✔
369
  }
370
  // 'log level must be a number'
371
  assert(Number.isFinite(resolvedLevel) && resolvedLevel >= 0);
10✔
372

373
  return resolvedLevel;
10✔
374
}
375

376
/**
377
 * "Normalizes" the various argument patterns into an object with known types
378
 * - log(logLevel, message, args) => {logLevel, message, args}
379
 * - log(message, args) => {logLevel: 0, message, args}
380
 * - log({logLevel, ...}, message, args) => {logLevel, message, args}
381
 * - log({logLevel, message, args}) => {logLevel, message, args}
382
 */
383
export function normalizeArguments(opts: {
384
  logLevel;
385
  message;
386
  collapsed?: boolean;
387
  args?: IArguments;
388
  opts?;
389
}): {
390
  logLevel: number;
391
  message: string;
392
  args: any[];
393
} {
394
  const {logLevel, message} = opts;
64✔
395
  opts.logLevel = normalizeLogLevel(logLevel);
64✔
396

397
  // We use `arguments` instead of rest parameters (...args) because IE
398
  // does not support the syntax. Rest parameters is transpiled to code with
399
  // perf impact. Doing it here instead avoids constructing args when logging is
400
  // disabled.
401
  // TODO - remove when/if IE support is dropped
402
  const args: any[] = opts.args ? Array.from(opts.args) : [];
64✔
403
  // args should only contain arguments that appear after `message`
404
  // eslint-disable-next-line no-empty
405
  while (args.length && args.shift() !== message) {}
64✔
406

407
  switch (typeof logLevel) {
64✔
408
    case 'string':
409
    case 'function':
410
      if (message !== undefined) {
40✔
411
        args.unshift(message);
3✔
412
      }
413
      opts.message = logLevel;
40✔
414
      break;
40✔
415

416
    case 'object':
417
      Object.assign(opts, logLevel);
4✔
418
      break;
4✔
419

420
    default:
421
  }
422

423
  // Resolve functions into strings by calling them
424
  if (typeof opts.message === 'function') {
64✔
425
    opts.message = opts.message();
4✔
426
  }
427
  const messageType = typeof opts.message;
64✔
428
  // 'log message must be a string' or object
429
  assert(messageType === 'string' || messageType === 'object');
64✔
430

431
  // original opts + normalized opts + opts arg + fixed up message
432
  return Object.assign(opts, {args}, opts.opts);
64✔
433
}
434

435
function decorateMessage(id, message, opts) {
436
  if (typeof message === 'string') {
49✔
437
    const time = opts.time ? leftPad(formatTime(opts.total)) : '';
45✔
438
    message = opts.time ? `${id}: ${time}  ${message}` : `${id}: ${message}`;
45✔
439
    message = addColor(message, opts.color, opts.background);
45✔
440
  }
441
  return message;
49✔
442
}
443

444
/** Use the asciify module to log an image under node.js */
445
function logImageInNode({image, message = '', scale = 1}) {
×
446
  asciify.nodeAsciifyImage({image, message, scale});
×
447
  return noop;
×
448
}
449

450
function logImageInBrowser({image, message = '', scale = 1}) {
×
451
  if (typeof image === 'string') {
×
452
    const img = new Image();
×
453
    img.onload = () => {
×
454
      const args = formatImage(img, message, scale);
×
455
      console.log(...args);
×
456
    };
457
    img.src = image;
×
458
    return noop;
×
459
  }
460
  const element = image.nodeName || '';
×
461
  if (element.toLowerCase() === 'img') {
×
462
    console.log(...formatImage(image, message, scale));
×
463
    return noop;
×
464
  }
465
  if (element.toLowerCase() === 'canvas') {
×
466
    const img = new Image();
×
467
    img.onload = () => console.log(...formatImage(img, message, scale));
×
468
    img.src = image.toDataURL();
×
469
    return noop;
×
470
  }
471
  return noop;
×
472
}
473

474
function getTableHeader(table: Table): string {
475
  for (const key in table) {
4✔
476
    for (const title in table[key]) {
4✔
477
      return title || 'untitled';
4!
478
    }
479
  }
480
  return 'empty';
×
481
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc