diff --git a/src/main/first-run.ts b/src/main/first-run.ts index 0a5678976..c119ea37a 100644 --- a/src/main/first-run.ts +++ b/src/main/first-run.ts @@ -1,7 +1,8 @@ import fs from 'node:fs'; import path from 'node:path'; import { app, dialog } from 'electron'; -import log from 'electron-log'; + +import { logError } from '../shared/logger'; export async function onFirstRunMaybe() { if (isFirstRun()) { @@ -49,7 +50,7 @@ function isFirstRun() { fs.writeFileSync(configPath, ''); } catch (err) { - log.error('First run: Unable to write firstRun file', err); + logError('isFirstRun', 'Unable to write firstRun file', err); } return true; diff --git a/src/main/updater.ts b/src/main/updater.ts index 82b4f5a77..949eae512 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -2,6 +2,8 @@ import log from 'electron-log'; import { autoUpdater } from 'electron-updater'; import type { Menubar } from 'menubar'; import { updateElectronApp } from 'update-electron-app'; + +import { logError, logInfo } from '../shared/logger'; import type MenuBuilder from './menu'; export default class Updater { @@ -18,29 +20,29 @@ export default class Updater { }); autoUpdater.on('checking-for-update', () => { - log.info('Auto Updater: Checking for update'); + logInfo('auto updater', 'Checking for update'); this.menuBuilder.setCheckForUpdatesMenuEnabled(false); }); - autoUpdater.on('error', (error) => { - log.error('Auto Updater: error checking for update', error); + autoUpdater.on('error', (err) => { + logError('auto updater', 'Error checking for update', err); this.menuBuilder.setCheckForUpdatesMenuEnabled(true); }); autoUpdater.on('update-available', () => { - log.info('Auto Updater: New update available'); + logInfo('auto updater', 'New update available'); menuBuilder.setUpdateAvailableMenuEnabled(true); this.menubar.tray.setToolTip('Gitify\nA new update is available'); }); autoUpdater.on('update-downloaded', () => { - log.info('Auto Updater: Update downloaded'); + logInfo('auto updater', 'Update downloaded'); menuBuilder.setUpdateReadyForInstallMenuEnabled(true); this.menubar.tray.setToolTip('Gitify\nA new update is ready to install'); }); autoUpdater.on('update-not-available', () => { - log.info('Auto Updater: update not available'); + logInfo('auto updater', 'Update not available'); this.menuBuilder.setCheckForUpdatesMenuEnabled(true); }); } diff --git a/src/main/utils.ts b/src/main/utils.ts index f86350bf4..fc531ad33 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -5,6 +5,8 @@ import { dialog, shell } from 'electron'; import log from 'electron-log'; import type { Menubar } from 'menubar'; +import { logError, logInfo } from '../shared/logger'; + export function takeScreenshot(mb: Menubar) { const date = new Date(); const dateStr = date.toISOString().replace(/:/g, '-'); @@ -12,7 +14,7 @@ export function takeScreenshot(mb: Menubar) { const capturedPicFilePath = `${os.homedir()}/${dateStr}-gitify-screenshot.png`; mb.window.capturePage().then((img) => { fs.writeFile(capturedPicFilePath, img.toPNG(), () => - log.info(`Screenshot saved ${capturedPicFilePath}`), + logInfo('takeScreenshot', `Screenshot saved ${capturedPicFilePath}`), ); }); } @@ -41,7 +43,11 @@ export function openLogsDirectory() { const logDirectory = path.dirname(log.transports.file?.getFile()?.path); if (!logDirectory) { - log.error('Could not find log directory!'); + logError( + 'openLogsDirectory', + 'Could not find log directory!', + new Error('Directory not found'), + ); return; } diff --git a/src/renderer/hooks/useNotifications.test.ts b/src/renderer/hooks/useNotifications.test.ts index 96b5437d8..be6a26e48 100644 --- a/src/renderer/hooks/useNotifications.test.ts +++ b/src/renderer/hooks/useNotifications.test.ts @@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios, { AxiosError } from 'axios'; import nock from 'nock'; -import log from 'electron-log'; +import * as logger from '../../shared/logger'; import { mockAuth, mockGitHubCloudAccount, @@ -17,7 +17,7 @@ import { Errors } from '../utils/errors'; import { useNotifications } from './useNotifications'; describe('renderer/hooks/useNotifications.ts', () => { - const logErrorSpy = jest.spyOn(log, 'error').mockImplementation(); + const logErrorSpy = jest.spyOn(logger, 'logError').mockImplementation(); beforeEach(() => { // axios will default to using the XHR adapter which can't be intercepted diff --git a/src/renderer/hooks/useNotifications.ts b/src/renderer/hooks/useNotifications.ts index 3f4d06d58..f57b35108 100644 --- a/src/renderer/hooks/useNotifications.ts +++ b/src/renderer/hooks/useNotifications.ts @@ -1,5 +1,6 @@ -import log from 'electron-log'; import { useCallback, useState } from 'react'; + +import { logError } from '../../shared/logger'; import type { Account, AccountNotifications, @@ -121,8 +122,9 @@ export const useNotifications = (): NotificationsState => { setNotifications(updatedNotifications); setTrayIconColor(updatedNotifications); } catch (err) { - log.error( - '[markNotificationsAsRead]: Error occurred while marking notifications as read', + logError( + 'markNotificationsAsRead', + 'Error occurred while marking notifications as read', err, ); } @@ -158,8 +160,9 @@ export const useNotifications = (): NotificationsState => { setNotifications(updatedNotifications); setTrayIconColor(updatedNotifications); } catch (err) { - log.error( - '[markNotificationsAsDone]: error occurred while marking notifications as done', + logError( + 'markNotificationsAsDone', + 'Error occurred while marking notifications as done', err, ); } @@ -186,9 +189,11 @@ export const useNotifications = (): NotificationsState => { await markNotificationsAsRead(state, [notification]); } } catch (err) { - log.error( - '[unsubscribeNotification]: error occurred while unsubscribing from notification thread', + logError( + 'unsubscribeNotification', + 'Error occurred while unsubscribing from notification thread', err, + notification, ); } diff --git a/src/renderer/routes/Accounts.tsx b/src/renderer/routes/Accounts.tsx index 2238401e4..fbd14122b 100644 --- a/src/renderer/routes/Accounts.tsx +++ b/src/renderer/routes/Accounts.tsx @@ -10,10 +10,10 @@ import { StarIcon, SyncIcon, } from '@primer/octicons-react'; - -import log from 'electron-log'; import { type FC, useCallback, useContext } from 'react'; import { useNavigate } from 'react-router-dom'; + +import { logError } from '../../shared/logger'; import { Header } from '../components/Header'; import { AuthMethodIcon } from '../components/icons/AuthMethodIcon'; import { AvatarIcon } from '../components/icons/AvatarIcon'; @@ -57,7 +57,7 @@ export const AccountsRoute: FC = () => { try { await loginWithGitHubApp(); } catch (err) { - log.error('Auth: failed to login with GitHub', err); + logError('loginWithGitHub', 'failed to login with GitHub', err); } }, []); diff --git a/src/renderer/routes/Login.tsx b/src/renderer/routes/Login.tsx index 2a661eb67..318f5f58a 100644 --- a/src/renderer/routes/Login.tsx +++ b/src/renderer/routes/Login.tsx @@ -1,7 +1,8 @@ import { KeyIcon, MarkGithubIcon, PersonIcon } from '@primer/octicons-react'; -import log from 'electron-log'; import { type FC, useCallback, useContext, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; + +import { logError } from '../../shared/logger'; import { Button } from '../components/buttons/Button'; import { LogoIcon } from '../components/icons/LogoIcon'; import { AppContext } from '../context/App'; @@ -23,7 +24,7 @@ export const LoginRoute: FC = () => { try { await loginWithGitHubApp(); } catch (err) { - log.error('Auth: failed to login with GitHub', err); + logError('loginWithGitHubApp', 'failed to login with GitHub', err); } }, [loginWithGitHubApp]); diff --git a/src/renderer/routes/LoginWithOAuthApp.tsx b/src/renderer/routes/LoginWithOAuthApp.tsx index de44218d2..51e3dc7e6 100644 --- a/src/renderer/routes/LoginWithOAuthApp.tsx +++ b/src/renderer/routes/LoginWithOAuthApp.tsx @@ -1,8 +1,9 @@ import { BookIcon, PersonIcon, SignInIcon } from '@primer/octicons-react'; -import log from 'electron-log'; import { type FC, useCallback, useContext } from 'react'; import { Form, type FormRenderProps } from 'react-final-form'; import { useNavigate } from 'react-router-dom'; + +import { logError } from '../../shared/logger'; import { Header } from '../components/Header'; import { Button } from '../components/buttons/Button'; import { FieldInput } from '../components/fields/FieldInput'; @@ -131,7 +132,7 @@ export const LoginWithOAuthAppRoute: FC = () => { await loginWithOAuthApp(data as LoginOAuthAppOptions); navigate(-1); } catch (err) { - log.error('Auth: Failed to login with oauth app', err); + logError('loginWithOAuthApp', 'Failed to login with OAuth App', err); } }, [loginWithOAuthApp], diff --git a/src/renderer/routes/LoginWithPersonalAccessToken.tsx b/src/renderer/routes/LoginWithPersonalAccessToken.tsx index cebc284fd..169df1a37 100644 --- a/src/renderer/routes/LoginWithPersonalAccessToken.tsx +++ b/src/renderer/routes/LoginWithPersonalAccessToken.tsx @@ -1,8 +1,9 @@ import { BookIcon, KeyIcon, SignInIcon } from '@primer/octicons-react'; -import log from 'electron-log'; import { type FC, useCallback, useContext, useState } from 'react'; import { Form, type FormRenderProps } from 'react-final-form'; import { useNavigate } from 'react-router-dom'; + +import { logError } from '../../shared/logger'; import { Header } from '../components/Header'; import { Button } from '../components/buttons/Button'; import { FieldInput } from '../components/fields/FieldInput'; @@ -129,7 +130,11 @@ export const LoginWithPersonalAccessTokenRoute: FC = () => { ); navigate(-1); } catch (err) { - log.error('Auth: failed to login with personal access token', err); + logError( + 'loginWithPersonalAccessToken', + 'Failed to login with PAT', + err, + ); setIsValidToken(false); } }, diff --git a/src/renderer/utils/api/client.test.ts b/src/renderer/utils/api/client.test.ts index ccb7ad2c5..f7b9acbd2 100644 --- a/src/renderer/utils/api/client.test.ts +++ b/src/renderer/utils/api/client.test.ts @@ -1,5 +1,5 @@ import axios, { type AxiosPromise, type AxiosResponse } from 'axios'; -import log from 'electron-log'; +import * as logger from '../../../shared/logger'; import { mockGitHubCloudAccount, mockGitHubEnterpriseServerAccount, @@ -268,7 +268,7 @@ describe('renderer/utils/api/client.ts', () => { }); it('should handle error', async () => { - const logErrorSpy = jest.spyOn(log, 'error').mockImplementation(); + const logErrorSpy = jest.spyOn(logger, 'logError').mockImplementation(); const apiRequestAuthMock = jest.spyOn(apiRequests, 'apiRequestAuth'); diff --git a/src/renderer/utils/api/client.ts b/src/renderer/utils/api/client.ts index bf39f204f..18b396128 100644 --- a/src/renderer/utils/api/client.ts +++ b/src/renderer/utils/api/client.ts @@ -1,6 +1,7 @@ import type { AxiosPromise } from 'axios'; -import log from 'electron-log'; import { print } from 'graphql/language/printer'; + +import { logError } from '../../../shared/logger'; import type { Account, Hostname, @@ -217,9 +218,9 @@ export async function getHtmlUrl(url: Link, token: Token): Promise { const response = (await apiRequestAuth(url, 'GET', token)).data; return response.html_url; } catch (err) { - log.error( - '[getHtmlUrl]: error occurred while fetching html url for', - url, + logError( + 'getHtmlUrl', + `error occurred while fetching html url for ${url}`, err, ); } @@ -271,10 +272,11 @@ export async function getLatestDiscussion( )[0] ?? null ); } catch (err) { - log.error( - '[getLatestDiscussion]: failed to fetch latest discussion for notification', - `[${notification.subject.type}]: ${notification.subject.title} for repository ${notification.repository.full_name}`, + logError( + 'getLatestDiscussion', + 'failed to fetch latest discussion for notification', err, + notification, ); } } diff --git a/src/renderer/utils/api/request.ts b/src/renderer/utils/api/request.ts index 3e0da29e9..39933e4fb 100644 --- a/src/renderer/utils/api/request.ts +++ b/src/renderer/utils/api/request.ts @@ -3,7 +3,8 @@ import axios, { type AxiosPromise, type Method, } from 'axios'; -import log from 'electron-log'; + +import { logError } from '../../../shared/logger'; import type { Link, Token } from '../../types'; import { getNextURLFromLinkHeader } from './utils'; @@ -73,7 +74,8 @@ export async function apiRequestAuth( nextUrl = getNextURLFromLinkHeader(response); } } catch (err) { - log.error('[apiRequestAuth]: API request failed:', err); + logError('apiRequestAuth', 'API request failed:', err); + throw err; } diff --git a/src/renderer/utils/auth/migration.ts b/src/renderer/utils/auth/migration.ts index 87b32e678..f662ab7a1 100644 --- a/src/renderer/utils/auth/migration.ts +++ b/src/renderer/utils/auth/migration.ts @@ -1,4 +1,4 @@ -import log from 'electron-log'; +import { logInfo } from '../../../shared/logger'; import type { Account, AuthState } from '../../types'; import { Constants } from '../constants'; import { loadState, saveState } from '../storage'; @@ -16,7 +16,10 @@ export async function migrateAuthenticatedAccounts() { return; } - log.info('Account Migration: Commencing authenticated accounts migration'); + logInfo( + 'migrateAuthenticatedAccounts', + 'Commencing authenticated accounts migration', + ); const migratedAccounts = await convertAccounts(existing.auth); @@ -24,7 +27,11 @@ export async function migrateAuthenticatedAccounts() { auth: { ...existing.auth, accounts: migratedAccounts }, settings: existing.settings, }); - log.info('Account Migration: Authenticated accounts migration complete'); + + logInfo( + 'migrateAuthenticatedAccounts', + 'Authenticated accounts migration complete', + ); } export function hasAccountsToMigrate(existingAuthState: AuthState): boolean { diff --git a/src/renderer/utils/auth/utils.ts b/src/renderer/utils/auth/utils.ts index 22d114ac4..c121ee0fd 100644 --- a/src/renderer/utils/auth/utils.ts +++ b/src/renderer/utils/auth/utils.ts @@ -1,7 +1,8 @@ import { BrowserWindow } from '@electron/remote'; import { format } from 'date-fns'; -import log from 'electron-log'; import semver from 'semver'; + +import { logError } from '../../../shared/logger'; import type { Account, AuthCode, @@ -178,9 +179,9 @@ export async function refreshAccount(account: Account): Promise { accountScopes.includes(scope), ); } catch (err) { - log.error( - '[refreshAccount]: failed to refresh account for user', - account.user.login, + logError( + 'refreshAccount', + `failed to refresh account for user ${account.user.login}`, err, ); } diff --git a/src/renderer/utils/helpers.test.ts b/src/renderer/utils/helpers.test.ts index 42547328c..8bb76ff99 100644 --- a/src/renderer/utils/helpers.test.ts +++ b/src/renderer/utils/helpers.test.ts @@ -6,7 +6,7 @@ import { ChevronLeftIcon, ChevronRightIcon, } from '@primer/octicons-react'; -import log from 'electron-log'; +import * as logger from '../../shared/logger'; import { defaultSettings } from '../context/App'; import type { Hostname, Link, SettingsState } from '../types'; import type { SubjectType } from '../typesGitHub'; @@ -487,7 +487,7 @@ describe('renderer/utils/helpers.ts', () => { }); it('defaults when exception handled during specialized html enrichment process', async () => { - const logErrorSpy = jest.spyOn(log, 'error').mockImplementation(); + const logErrorSpy = jest.spyOn(logger, 'logError').mockImplementation(); const subject = { title: 'generate github web url unit tests', diff --git a/src/renderer/utils/helpers.ts b/src/renderer/utils/helpers.ts index 2017017e5..6954f13e6 100644 --- a/src/renderer/utils/helpers.ts +++ b/src/renderer/utils/helpers.ts @@ -4,7 +4,8 @@ import { ChevronRightIcon, } from '@primer/octicons-react'; import { formatDistanceToNowStrict, parseISO } from 'date-fns'; -import log from 'electron-log'; + +import { logError, logWarn } from '../../shared/logger'; import { defaultSettings } from '../context/App'; import type { Chevron, Hostname, Link, SettingsState } from '../types'; import type { Notification } from '../typesGitHub'; @@ -151,14 +152,16 @@ export async function generateGitHubWebUrl( } } } catch (err) { - log.error( - '[generateGitHubWebUrl]: failed to resolve specific notification html url for', - `[${notification.subject.type}]: ${notification.subject.title} for repository ${notification.repository.full_name}`, + logError( + 'generateGitHubWebUrl', + 'Failed to resolve specific notification html url for', err, + notification, ); - log.warn( - 'Will fall back to opening repository root url for', - notification.repository.full_name, + + logWarn( + 'generateGitHubWebUrl', + `Falling back to repository root url: ${notification.repository.full_name}`, ); } diff --git a/src/renderer/utils/notifications.ts b/src/renderer/utils/notifications.ts index 1b0a41b0b..091f988bc 100644 --- a/src/renderer/utils/notifications.ts +++ b/src/renderer/utils/notifications.ts @@ -1,5 +1,6 @@ import path from 'node:path'; -import log from 'electron-log'; + +import { logError, logWarn } from '../../shared/logger'; import type { AccountNotifications, GitifyState, @@ -157,10 +158,12 @@ export async function getAllNotifications( error: null, }; } catch (err) { - log.error( - '[getAllNotifications]: error occurred while fetching account notifications', + logError( + 'getAllNotifications', + 'error occurred while fetching account notifications', err, ); + return { account: accountNotifications.account, notifications: [], @@ -188,12 +191,17 @@ export async function enrichNotifications( try { additionalSubjectDetails = await getGitifySubjectDetails(notification); } catch (err) { - log.error( - '[enrichNotifications] failed to enrich notification details for', - `[${notification.subject.type}]: ${notification.subject.title} for repository ${notification.repository.full_name}`, + logError( + 'enrichNotifications', + 'failed to enrich notification details for', err, + notification, + ); + + logWarn( + 'enrichNotifications', + 'Continuing with base notification details', ); - log.warn('Continuing with base notification details'); } return { diff --git a/src/renderer/utils/subject.test.ts b/src/renderer/utils/subject.test.ts index a925915ee..d2f42843f 100644 --- a/src/renderer/utils/subject.test.ts +++ b/src/renderer/utils/subject.test.ts @@ -31,7 +31,7 @@ const mockDiscussionAuthor: DiscussionAuthor = { avatar_url: 'https://avatars.githubusercontent.com/u/123456789?v=4' as Link, type: 'User', }; -import log from 'electron-log'; +import * as logger from '../../shared/logger'; describe('renderer/utils/subject.ts', () => { beforeEach(() => { @@ -1152,7 +1152,7 @@ describe('renderer/utils/subject.ts', () => { describe('Error', () => { it('catches error and logs message', async () => { - const logErrorSpy = jest.spyOn(log, 'error').mockImplementation(); + const logErrorSpy = jest.spyOn(logger, 'logError').mockImplementation(); const mockError = new Error('Test error'); const mockNotification = partialMockNotification({ @@ -1172,9 +1172,10 @@ describe('renderer/utils/subject.ts', () => { await getGitifySubjectDetails(mockNotification); expect(logErrorSpy).toHaveBeenCalledWith( - '[getGitifySubjectDetails]: failed to fetch details for notification for', - '[Issue]: This issue will throw an error for repository gitify-app/notifications-test', + 'getGitifySubjectDetails', + 'failed to fetch details for notification for', mockError, + mockNotification, ); }); }); diff --git a/src/renderer/utils/subject.ts b/src/renderer/utils/subject.ts index 223ef60ed..cae5cdbfc 100644 --- a/src/renderer/utils/subject.ts +++ b/src/renderer/utils/subject.ts @@ -1,4 +1,4 @@ -import log from 'electron-log'; +import { logError } from '../../shared/logger'; import type { Link } from '../types'; import type { CheckSuiteAttributes, @@ -49,10 +49,11 @@ export async function getGitifySubjectDetails( return null; } } catch (err) { - log.error( - '[getGitifySubjectDetails]: failed to fetch details for notification for', - `[${notification.subject.type}]: ${notification.subject.title} for repository ${notification.repository.full_name}`, + logError( + 'getGitifySubjectDetails', + 'failed to fetch details for notification for', err, + notification, ); } } diff --git a/src/shared/logger.test.ts b/src/shared/logger.test.ts new file mode 100644 index 000000000..468741f2f --- /dev/null +++ b/src/shared/logger.test.ts @@ -0,0 +1,79 @@ +import log from 'electron-log'; + +import { mockSingleNotification } from '../renderer/utils/api/__mocks__/response-mocks'; +import { logError, logInfo, logWarn } from './logger'; + +describe('renderer/utils/logger.ts', () => { + const logInfoSpy = jest.spyOn(log, 'info').mockImplementation(); + const logWarnSpy = jest.spyOn(log, 'warn').mockImplementation(); + const logErrorSpy = jest.spyOn(log, 'error').mockImplementation(); + + const mockError = new Error('baz'); + + beforeEach(() => { + logInfoSpy.mockReset(); + logWarnSpy.mockReset(); + logErrorSpy.mockReset(); + }); + + describe('logInfo', () => { + it('log info without notification', () => { + logInfo('foo', 'bar'); + + expect(logInfoSpy).toHaveBeenCalledTimes(1); + expect(logInfoSpy).toHaveBeenCalledWith('[foo]', 'bar'); + }); + + it('log info with notification', () => { + logInfo('foo', 'bar', mockSingleNotification); + + expect(logInfoSpy).toHaveBeenCalledTimes(1); + expect(logInfoSpy).toHaveBeenCalledWith( + '[foo]', + 'bar', + '[Issue >> gitify-app/notifications-test >> I am a robot and this is a test!]', + ); + }); + }); + + describe('logWarn', () => { + it('log warn without notification', () => { + logWarn('foo', 'bar'); + + expect(logWarnSpy).toHaveBeenCalledTimes(1); + expect(logWarnSpy).toHaveBeenCalledWith('[foo]', 'bar'); + }); + + it('log warn with notification', () => { + logWarn('foo', 'bar', mockSingleNotification); + + expect(logWarnSpy).toHaveBeenCalledTimes(1); + expect(logWarnSpy).toHaveBeenCalledWith( + '[foo]', + 'bar', + '[Issue >> gitify-app/notifications-test >> I am a robot and this is a test!]', + ); + }); + }); + + describe('logError', () => { + it('log error without notification', () => { + logError('foo', 'bar', mockError); + + expect(logErrorSpy).toHaveBeenCalledTimes(1); + expect(logErrorSpy).toHaveBeenCalledWith('[foo]', 'bar', mockError); + }); + + it('log error with notification', () => { + logError('foo', 'bar', mockError, mockSingleNotification); + + expect(logErrorSpy).toHaveBeenCalledTimes(1); + expect(logErrorSpy).toHaveBeenCalledWith( + '[foo]', + 'bar', + '[Issue >> gitify-app/notifications-test >> I am a robot and this is a test!]', + mockError, + ); + }); + }); +}); diff --git a/src/shared/logger.ts b/src/shared/logger.ts new file mode 100644 index 000000000..f9242160f --- /dev/null +++ b/src/shared/logger.ts @@ -0,0 +1,51 @@ +import log from 'electron-log'; + +import type { Notification } from '../renderer/typesGitHub'; + +export function logInfo( + type: string, + message: string, + notification?: Notification, +) { + logMessage(log.info, type, message, null, notification); +} + +export function logWarn( + type: string, + message: string, + notification?: Notification, +) { + logMessage(log.warn, type, message, null, notification); +} + +export function logError( + type: string, + message: string, + err: Error, + notification?: Notification, +) { + logMessage(log.error, type, message, err, notification); +} + +function logMessage( + // biome-ignore lint/suspicious/noExplicitAny: + logFunction: (...params: any[]) => void, + type: string, + message: string, + err?: Error, + notification?: Notification, +) { + const args: (string | Error)[] = [`[${type}]`, message]; + + if (notification) { + args.push( + `[${notification.subject.type} >> ${notification.repository.full_name} >> ${notification.subject.title}]`, + ); + } + + if (err) { + args.push(err); + } + + logFunction(...args); +}