diff --git a/packages/client-app/internal_packages/sync-health-checker/lib/main.es6 b/packages/client-app/internal_packages/sync-health-checker/lib/main.es6 new file mode 100644 index 000000000..e7ca045d1 --- /dev/null +++ b/packages/client-app/internal_packages/sync-health-checker/lib/main.es6 @@ -0,0 +1,9 @@ +import SyncHealthChecker from './sync-health-checker' + +export function activate() { + SyncHealthChecker.start() +} + +export function deactivate() { + SyncHealthChecker.stop() +} diff --git a/packages/client-app/internal_packages/sync-health-checker/lib/sync-health-checker.es6 b/packages/client-app/internal_packages/sync-health-checker/lib/sync-health-checker.es6 new file mode 100644 index 000000000..70ba7dbe9 --- /dev/null +++ b/packages/client-app/internal_packages/sync-health-checker/lib/sync-health-checker.es6 @@ -0,0 +1,111 @@ +import {ipcRenderer} from 'electron' +import {AccountStore, Actions, NylasAPI, NylasAPIRequest} from 'nylas-exports' + +const CHECK_HEALTH_INTERVAL = 5 * 60 * 1000; + +class SyncHealthChecker { + constructor() { + this._lastSyncActivity = null + this._interval = null + } + + start() { + if (this._interval) { + console.warn('SyncHealthChecker has already been started') + } else { + this._interval = setInterval(this._checkSyncHealth, CHECK_HEALTH_INTERVAL) + } + } + + stop() { + clearInterval(this._interval) + this._interval = null + } + + // This is a separate function so the request can be manipulated in the specs + _buildRequest = () => { + return new NylasAPIRequest({ + api: NylasAPI, + options: { + accountId: AccountStore.accounts()[0].id, + path: `/health`, + }, + }); + } + + _checkSyncHealth = async () => { + try { + const request = this._buildRequest() + const response = await request.run() + this._lastSyncActivity = response + } catch (err) { + if (/ECONNREFUSED/i.test(err.toString())) { + this._onWorkerWindowUnavailable() + } else { + err.message = `Error checking sync health: ${err.message}` + NylasEnv.reportError(err) + } + } + } + + _onWorkerWindowUnavailable() { + let extraData = {}; + + // Extract data that we want to report. We'll report the entire + // _lastSyncActivity object, but it'll probably be useful if we can segment + // by the data in the oldest or newest entry, so we report those as + // individual values too. + const lastActivityEntries = Object.entries(this._lastSyncActivity || {}) + if (lastActivityEntries.length > 0) { + const times = lastActivityEntries.map((entry) => entry[1].time) + const now = Date.now() + + const maxTime = Math.max(...times) + const mostRecentEntry = lastActivityEntries.find((entry) => entry[1].time === maxTime) + const [mostRecentActivityAccountId, { + activity: mostRecentActivity, + time: mostRecentActivityTime, + }] = mostRecentEntry; + const mostRecentDuration = now - mostRecentActivityTime + + const minTime = Math.min(...times) + const leastRecentEntry = lastActivityEntries.find((entry) => entry[1].time === minTime) + const [leastRecentActivityAccountId, { + activity: leastRecentActivity, + time: leastRecentActivityTime, + }] = leastRecentEntry; + const leastRecentDuration = now - leastRecentActivityTime + + extraData = { + mostRecentActivity, + mostRecentActivityTime, + mostRecentActivityAccountId, + mostRecentDuration, + leastRecentActivity, + leastRecentActivityTime, + leastRecentActivityAccountId, + leastRecentDuration, + } + } + + NylasEnv.reportError(new Error('Worker window was unavailable'), { + // This information isn't as useful in Sentry, but include it here until + // the data is actually sent to Mixpanel. (See the TODO below) + lastActivityPerAccount: this._lastSyncActivity, + ...extraData, + }) + + // TODO: This doesn't make it to Mixpanel because our analytics process + // lives in the worker window. We should move analytics to the main process. + // https://phab.nylas.com/T8029 + Actions.recordUserEvent('Worker Window Unavailable', { + lastActivityPerAccount: this._lastSyncActivity, + ...extraData, + }) + + console.log(`Detected worker window was unavailable. Restarting it.`, this._lastSyncActivity) + ipcRenderer.send('ensure-worker-window') + } +} + +export default new SyncHealthChecker() diff --git a/packages/client-app/internal_packages/sync-health-checker/package.json b/packages/client-app/internal_packages/sync-health-checker/package.json new file mode 100644 index 000000000..f4682568b --- /dev/null +++ b/packages/client-app/internal_packages/sync-health-checker/package.json @@ -0,0 +1,11 @@ +{ + "name": "sync-health-checker", + "version": "0.1.0", + "main": "./lib/main", + "description": "Periodically ping the sync process to ensure it's running", + "license": "GPL-3.0", + "private": true, + "engines": { + "nylas": "*" + } +} diff --git a/packages/client-app/src/browser/application.es6 b/packages/client-app/src/browser/application.es6 index 3768dceb4..fd36e19b9 100644 --- a/packages/client-app/src/browser/application.es6 +++ b/packages/client-app/src/browser/application.es6 @@ -465,6 +465,10 @@ export default class Application extends EventEmitter { } }); + ipcMain.on('ensure-worker-window', () => { + this.windowManager.ensureWindow(WindowManager.WORK_WINDOW) + }) + ipcMain.on('inline-style-parse', (event, {html, key}) => { const juice = require('juice'); let out = null;