diff --git a/src/main/util/rateLimiter.ts b/src/main/util/rateLimiter.ts new file mode 100644 index 000000000..a97aba0f5 --- /dev/null +++ b/src/main/util/rateLimiter.ts @@ -0,0 +1,60 @@ +/** + * Code initially taken from https://github.com/wankdanker/node-function-rate-limit + * MIT License - Copyright (c) 2012 Daniel L. VerWeire + */ + +/** + * Rate limiting utility function + * @param limitCount Maximum number of calls allowed within the interval + * @param limitInterval Time interval in milliseconds + * @param fn Function to be rate limited + * @returns Rate limited version of the function + */ +export function rateLimit any>( + limitCount: number, + limitInterval: number, + fn: T +): T { + type Context = ThisParameterType; + type Args = Parameters; + type QueueItem = [Context, Args]; + + const fifo: QueueItem[] = []; + let count = limitCount; + + function callNext(args?: QueueItem) { + setTimeout(() => { + if (fifo.length > 0) { + callNext(); + } else { + count = count + 1; + } + }, limitInterval); + + const callArgs = fifo.shift(); + + // if there is no next item in the queue + // and we were called with args, trigger function immediately + if (!callArgs && args) { + fn.apply(args[0], args[1]); + return; + } + + if (callArgs) { + fn.apply(callArgs[0], callArgs[1]); + } + } + + return function rateLimitedFunction(this: Context, ...args: Args): ReturnType { + const ctx = this; + + if (count <= 0) { + fifo.push([ctx, args]); + return undefined as ReturnType; + } + + count = count - 1; + callNext([ctx, args]); + return undefined as ReturnType; + } as T; +} \ No newline at end of file diff --git a/src/main/util/sentryWinstonTransport.ts b/src/main/util/sentryWinstonTransport.ts index 6a9d1ab12..eb74ce611 100644 --- a/src/main/util/sentryWinstonTransport.ts +++ b/src/main/util/sentryWinstonTransport.ts @@ -3,6 +3,7 @@ */ import * as Sentry from '@sentry/electron/main'; import TransportStream from 'winston-transport'; +import { rateLimit } from './rateLimiter.js'; // import { LEVEL } from 'triple-beam'; enum SentrySeverity { @@ -47,8 +48,8 @@ class ExtendedError extends Error { export default class SentryTransport extends TransportStream { public silent = false; - private levelsMap: SeverityOptions = {}; + private normalLogRateLimiter: (info: any, callback: () => void) => void; public constructor(opts?: SentryTransportOptions) { super(opts); @@ -56,12 +57,15 @@ export default class SentryTransport extends TransportStream { this.levelsMap = this.setLevelsMap(opts?.levelsMap); this.silent = opts?.silent || false; + // Only rate limit normal logs, not errors + this.normalLogRateLimiter = rateLimit(10, 1000, this.processLog.bind(this)); + if (!opts || !opts.skipSentryInit) { Sentry.init(SentryTransport.withDefaults(opts?.sentry || {})); } } - public log(info: any, callback: () => void) { + private processLog(info: any, callback: () => void) { setImmediate(() => { this.emit('logged', info); }); @@ -99,19 +103,30 @@ export default class SentryTransport extends TransportStream { // // ... // }); - // Capturing Errors / Exceptions + // Capturing Errors / Exceptions - bypass rate limiting if (SentryTransport.shouldLogException(sentryLevel)) { const error = Object.values(info).find((value) => value instanceof Error) ?? new ExtendedError(info); Sentry.captureException(error, { tags }); - - return callback(); + callback(); + return; } - // Capturing Messages + // Normal messages go through rate limiting Sentry.captureMessage(message, sentryLevel); - return callback(); + callback(); + } + + public log(info: any, callback: () => void) { + // Errors and fatal logs bypass rate limiting + if (SentryTransport.shouldLogException(this.levelsMap[info.level])) { + this.processLog(info, callback); + return; + } + + // Normal logs are rate limited + this.normalLogRateLimiter(info, callback); } end(...args: any[]) {