1 /* eslint-disable no-console */
2 import { getIsIframe } from '@proton/shared/lib/helpers/browser';
3 import { traceError } from '@proton/shared/lib/helpers/sentry';
5 import { Availability, AvailabilityTypes } from './availability';
7 type DebugLogDetail = {
12 // This is to be safe and to avoid [object Object] appearing, forces the consumer to make the necessary changes for compatibility
13 interface HasToString {
14 toString: () => string;
17 type Args = (string | number | boolean | HasToString | object | undefined | null)[];
18 type LogTypes = 'error' | 'warn' | 'debug' | 'info';
20 const toString = (args: Args): string => {
23 if (typeof arg === 'object' && arg.toString() === '[object Object]') {
24 return Object.keys(arg)
26 if (['string', 'number', 'boolean'].includes(typeof arg[key])) {
27 return `${key}:${arg[key]}`;
32 return arg?.toString();
37 const report = (tag: string, ...args: Args) => {
38 const isMainFrame = !getIsIframe();
41 const error = args.find((arg) => arg instanceof Error);
42 traceError(error || new Error(toString(args)), {
48 ...args.filter((arg) => !(arg instanceof Error)),
53 // child frames bubble up the report to parent
54 window.parent.postMessage({ type: '@proton/utils/logs:report', tag, args }, '*');
59 * Interface for a logger system that supports different levels of logging.
60 * The logger can work within iframes.
61 * All logs are kept in memory and can be sent to customer support at the choice of the customer.
62 * No private or sensitive information should be ever logged using any of these methods.
63 * PII is what that ties back to the user. Eg: account information, keys, media metadata (filename, size, etc..), network setup, browser setup and so on.
65 export interface LoggerInterface {
67 * Logs debug-level messages, useful for development and troubleshooting.
68 * @param args - The messages or objects to log. Do not include private or sensitive information.
70 debug(...args: Args): void;
73 * Logs informational messages, typically used for general logging of application flow.
74 * @param args - The messages or objects to log. Do not include private or sensitive information.
76 info(...args: Args): void;
79 * Logs warning messages, indicating a potential issue or important situation to monitor.
80 * @param args - The messages or objects to log. Do not include private or sensitive information.
82 warn(...args: Args): void;
85 * Logs error messages, typically used for logging errors and exceptions.
86 * @param args - The messages or objects to log. Do not include private or sensitive information.
88 error(...args: Args): void;
91 * Initiates the download of the logged messages in memory.
96 * Clears all logged messages from the logger.
100 setEnabled(enabled: boolean): void;
103 export class Logger implements LoggerInterface {
104 private identifier: string;
106 private stack: [string, DebugLogDetail][] = [];
108 private verbose: boolean = false;
110 private enabled: boolean = true;
115 private limit = 10_000
117 this.identifier = identifier;
118 this.verbose = typeof debugKey !== 'undefined' && localStorage.getItem(debugKey) === 'true';
122 setEnabled(enabled: boolean) {
123 this.enabled = enabled;
126 public debug(...args: Args): void {
128 console.log(...this.logWithColorParams(), ...args);
129 this.save(toString(args), 'warn');
133 public info(...args: Args): void {
135 console.log(...this.logWithColorParams(), ...args);
136 this.save(toString(args), 'info');
140 public warn(...args: Args): void {
142 console.warn(...args);
143 this.save(toString(args), 'warn');
147 public error(...args: Args): void {
149 console.error(...args);
150 this.save(toString(args), 'error');
151 report(this.identifier, ...args);
152 Availability.mark(AvailabilityTypes.ERROR);
156 public downloadLogs(): void {
157 const logData = this.getLogs();
162 const elm = document.createElement('a');
163 elm.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(logData));
164 elm.setAttribute('download', `proton-${this.identifier}-debug.log`);
165 elm.style.display = 'none';
166 document.body.appendChild(elm);
168 document.body.removeChild(elm);
171 public clearLogs(): void {
172 this.stack.splice(0, this.stack.length);
175 public getLogs(): string {
177 for (const [message, details] of this.stack) {
178 buffer += `${JSON.stringify(Object.assign(details, { message }))}\n`;
183 private save(message: string, type: LogTypes, time = new Date()): void {
184 const [lastMessage, lastDetails] = this.stack.at(this.stack.length - 1) || [];
185 // To avoid logging a lot of the same message we log a timer if previous message is exactly the same
186 if (lastMessage === message && lastDetails) {
187 this.stack.splice(this.stack.length - 1, 1, [
191 times: [...lastDetails.times, time],
204 if (this.stack.length > this.limit) {
209 private logWithColorParams(): string[] {
210 const date = new Date();
211 const timeString = `${date.toLocaleTimeString().replace(' PM', '').replace(' AM', '')}.${date.getMilliseconds()}`;
212 return [`%c${this.identifier}%c${timeString}`, 'color: font-weight: bold; margin-right: 4px', 'color: gray'];
215 private listen(): void {
216 const isMainFrame = !getIsIframe();
218 window.addEventListener('keydown', (event) => {
219 // Mac/Windows/Linux: Press Ctrl + Shift + H (uppercase)
220 if (event.ctrlKey && event.shiftKey && event.key === 'H') {
221 // Download logs from current frame
224 // If we're in a child frame, we postMessage to the parent
226 window.parent.postMessage({ type: '@proton/utils/logs:downloadLogs' }, '*');
231 // For main frame, we listen to child frames padding over the keyboard event
233 window.addEventListener('message', (event) => {
234 if (event.data && event.data.type === '@proton/utils/logs:downloadLogs') {
237 if (event.data && event.data.type === '@proton/utils/logs:report') {
239 typeof event.data.tag === 'string' &&
241 event.data.args != null &&
242 typeof event.data.args[Symbol.iterator] === 'function'
244 report(event.data.tag, ...event.data.args);
248 new Error('@proton/utils/logs:report message does not contain args or is not spreadable')