diff --git a/packages/electron-filestore/filestore.js b/packages/electron-filestore/filestore.js index 543a6b8df..6cce299f9 100644 --- a/packages/electron-filestore/filestore.js +++ b/packages/electron-filestore/filestore.js @@ -13,8 +13,11 @@ class FileStore { sessions: join(base, 'sessions'), runinfo: join(base, 'runinfo'), device: join(base, 'device.json'), + lastRunInfo: join(base, 'last-run-info.json'), minidumps: crashDir } + + this._appRunMetadata = { [identifierKey]: createIdentifier() } } // Create directory layout @@ -134,8 +137,35 @@ class FileStore { return device } - createAppRunMetadata () { - return { [identifierKey]: createIdentifier() } + getLastRunInfo () { + try { + // similar to getDeviceInfo - the lastRunInfo must be available during tha app-launch phase + // as such we use readFileSync to ensure that the data is loaded immediately + const contents = readFileSync(this._paths.lastRunInfo) + const lastRunInfo = JSON.parse(contents) + + if (typeof lastRunInfo.crashed === 'boolean' && + typeof lastRunInfo.crashedDuringLaunch === 'boolean' && + typeof lastRunInfo.consecutiveLaunchCrashes === 'number') { + return lastRunInfo + } + } catch (e) { + } + + return null + } + + setLastRunInfo (lastRunInfo = { crashed: false, crashedDuringLaunch: false, consecutiveLaunchCrashes: 0 }) { + try { + mkdirSync(dirname(this._paths.lastRunInfo), { recursive: true }) + writeFileSync(this._paths.lastRunInfo, JSON.stringify(lastRunInfo)) + } catch (e) { + } + return lastRunInfo + } + + getAppRunMetadata () { + return this._appRunMetadata } } diff --git a/packages/electron-filestore/test/filestore.test.ts b/packages/electron-filestore/test/filestore.test.ts index 1256e121c..a6ee34532 100644 --- a/packages/electron-filestore/test/filestore.test.ts +++ b/packages/electron-filestore/test/filestore.test.ts @@ -216,9 +216,9 @@ describe('FileStore', () => { }) }) - describe('createAppRunMetadata()', () => { + describe('getAppRunMetadata()', () => { it('generates a key in an expected format', () => { - const metadata = store.createAppRunMetadata() + const metadata = store.getAppRunMetadata() expect(metadata.bugsnag_crash_id).toMatch(/^[0-9a-z]{64}$/) }) }) diff --git a/packages/electron/src/client/main.js b/packages/electron/src/client/main.js index 14bb11c6a..f772c1b69 100644 --- a/packages/electron/src/client/main.js +++ b/packages/electron/src/client/main.js @@ -4,6 +4,10 @@ const Client = require('@bugsnag/core/client') const Event = require('@bugsnag/core/event') const Breadcrumb = require('@bugsnag/core/breadcrumb') const Session = require('@bugsnag/core/session') +const { + plugin: PluginClientStatePersistence, + NativeClient +} = require('@bugsnag/plugin-electron-client-state-persistence') const makeDelivery = require('@bugsnag/delivery-electron') const { FileStore } = require('@bugsnag/electron-filestore') @@ -25,8 +29,6 @@ module.exports = (opts) => { opts.projectRoot = normalizePath(opts.projectRoot) } - const { plugin: PluginClientStatePersistence, NativeClient } = require('@bugsnag/plugin-electron-client-state-persistence') - // main internal plugins go here const internalPlugins = [ // Plugins after the "FirstPlugin" will run in the main process for renderer @@ -38,7 +40,7 @@ module.exports = (opts) => { require('@bugsnag/plugin-electron-ipc'), require('@bugsnag/plugin-node-uncaught-exception'), require('@bugsnag/plugin-node-unhandled-rejection'), - require('@bugsnag/plugin-electron-app')(NativeClient, process, electron.app, electron.BrowserWindow), + require('@bugsnag/plugin-electron-app')(NativeClient, process, electron.app, electron.BrowserWindow, filestore), require('@bugsnag/plugin-electron-app-breadcrumbs')(electron.app, electron.BrowserWindow), require('@bugsnag/plugin-electron-device')(electron.app, electron.screen, process, filestore, NativeClient, electron.powerMonitor), require('@bugsnag/plugin-electron-session')(electron.app, electron.BrowserWindow, filestore), @@ -58,14 +60,13 @@ module.exports = (opts) => { const bugsnag = new Client(opts, schema, internalPlugins, require('../id')) - bugsnag._setDelivery(makeDelivery(filestore, electron.net, electron.app)) - filestore.init().catch(err => bugsnag._logger.warn('Failed to init crash FileStore directories', err)) // expose markLaunchComplete as a method on the Bugsnag client/facade const electronApp = bugsnag.getPlugin('electronApp') const { markLaunchComplete } = electronApp bugsnag.markLaunchComplete = markLaunchComplete + bugsnag._setDelivery(makeDelivery(filestore, electron.net, electron.app)) bugsnag._logger.debug('Loaded! In main process.') if (bugsnag._isBreadcrumbTypeEnabled('state')) { diff --git a/packages/electron/src/notifier.js b/packages/electron/src/notifier.js index 10e86ed99..a8a0b9094 100644 --- a/packages/electron/src/notifier.js +++ b/packages/electron/src/notifier.js @@ -10,6 +10,7 @@ const { Client, Event, Breadcrumb, Session } = createClient const Bugsnag = { _client: null, + lastRunInfo: null, start: (opts) => { if (Bugsnag._client) { Bugsnag._client._logger.warn('Bugsnag.start() was called more than once. Ignoring.') @@ -23,6 +24,15 @@ const Bugsnag = { // create the relevant client for the detected environment Bugsnag._client = createClient(opts) + Object.defineProperty(Bugsnag, 'lastRunInfo', { + get: isMain + ? () => Bugsnag._client.lastRunInfo + : () => { + Bugsnag._client._logger.warn('Bugsnag.lastRunInfo can only be accessed in the main process') + return null + } + }) + return Bugsnag._client } } diff --git a/packages/electron/types/notifier.d.ts b/packages/electron/types/notifier.d.ts index b6b6bdf42..33d99b45e 100644 --- a/packages/electron/types/notifier.d.ts +++ b/packages/electron/types/notifier.d.ts @@ -35,8 +35,15 @@ interface RendererConfig extends AllowedRendererConfig { codeBundleId?: string } +interface LastRunInfo { + crashed: boolean + crashedDuringLaunch: boolean + consecutiveLaunchCrashes: number +} + declare class ElectronClient extends Client { markLaunchComplete: () => void + readonly lastRunInfo: LastRunInfo | null } interface ElectronBugsnagStatic extends ElectronClient { diff --git a/packages/plugin-electron-app/app.js b/packages/plugin-electron-app/app.js index 9390c7745..7a329d938 100644 --- a/packages/plugin-electron-app/app.js +++ b/packages/plugin-electron-app/app.js @@ -18,6 +18,14 @@ const createAppUpdater = (client, NativeClient, app) => newProperties => { } } +const createLastRunInfoUpdater = (client, NativeClient) => lastRunInfo => { + try { + NativeClient.setLastRunInfo(JSON.stringify(lastRunInfo)) + } catch (err) { + client._logger.error(err) + } +} + const getInstalledFromStore = process => { if (process.mas) { return 'mac' @@ -30,15 +38,39 @@ const getInstalledFromStore = process => { return undefined } -module.exports = (NativeClient, process, electronApp, BrowserWindow, NativeApp = native) => ({ +module.exports = (NativeClient, process, electronApp, BrowserWindow, filestore, NativeApp = native) => ({ name: 'electronApp', load (client) { const app = {} + const lastRunInfo = filestore.getLastRunInfo() const updateApp = createAppUpdater(client, NativeClient, app) + const updateNextCrashLastRunInfo = createLastRunInfoUpdater(client, NativeClient) + + client.lastRunInfo = lastRunInfo + + updateNextCrashLastRunInfo({ + crashed: true, + crashedDuringLaunch: true, + consecutiveLaunchCrashes: lastRunInfo && lastRunInfo.consecutiveLaunchCrashes + ? lastRunInfo.consecutiveLaunchCrashes + 1 + : 1 + }) const markLaunchComplete = () => { if (app.isLaunching) { + filestore.setLastRunInfo({ + crashed: false, + crashedDuringLaunch: false, + consecutiveLaunchCrashes: 0 + }) + updateApp({ isLaunching: false }) + // mark lastRunInfo for possible crash in the NativeClient - only applied for a native crash + updateNextCrashLastRunInfo({ + crashed: true, + crashedDuringLaunch: false, + consecutiveLaunchCrashes: 0 + }) } } @@ -131,7 +163,6 @@ module.exports = (NativeClient, process, electronApp, BrowserWindow, NativeApp = }) client._app = app - return { markLaunchComplete } }, configSchema: { diff --git a/packages/plugin-electron-app/test/app.test.ts b/packages/plugin-electron-app/test/app.test.ts index df29b74e8..52e9169e1 100644 --- a/packages/plugin-electron-app/test/app.test.ts +++ b/packages/plugin-electron-app/test/app.test.ts @@ -757,6 +757,7 @@ interface MakeClientOptions { NativeApp?: any process?: any config?: { launchDurationMillis: number|undefined } + filestore?: any } function makeClient ({ @@ -765,17 +766,27 @@ function makeClient ({ NativeClient = makeNativeClient(), process = makeProcess(), config = { launchDurationMillis: 0 }, - NativeApp = makeNativeApp() + NativeApp = makeNativeApp(), + filestore = makeFileStore() }: MakeClientOptions = {}): ReturnType { return makeClientForPlugin({ config, - plugin: plugin(NativeClient, process, electronApp, BrowserWindow, NativeApp) + plugin: plugin(NativeClient, process, electronApp, BrowserWindow, filestore, NativeApp) }) } function makeNativeClient () { return { - setApp: jest.fn() + install: jest.fn(), + setApp: jest.fn(), + setLastRunInfo: jest.fn() + } +} + +function makeFileStore () { + return { + getLastRunInfo: jest.fn().mockReturnValue({ crashed: false, crashedDuringLaunch: false, consecutiveLaunchCrashes: 0 }), + setLastRunInfo: jest.fn() } } diff --git a/packages/plugin-electron-client-state-persistence/src/api.c b/packages/plugin-electron-client-state-persistence/src/api.c index fe02503f0..be782c739 100644 --- a/packages/plugin-electron-client-state-persistence/src/api.c +++ b/packages/plugin-electron-client-state-persistence/src/api.c @@ -133,14 +133,14 @@ static napi_value Uninstall(napi_env env, napi_callback_info info) { } static napi_value Install(napi_env env, napi_callback_info info) { - size_t argc = 3; - napi_value args[3]; + size_t argc = 4; + napi_value args[4]; napi_status status = napi_get_cb_info(env, info, &argc, args, NULL, NULL); assert(status == napi_ok); - if (argc < 2) { + if (argc < 3) { napi_throw_type_error(env, NULL, - "Wrong number of arguments, expected 2 or 3"); + "Wrong number of arguments, expected 3 or 4"); return NULL; } @@ -152,9 +152,13 @@ static napi_value Install(napi_env env, napi_callback_info info) { status = napi_typeof(env, args[1], &valuetype1); assert(status == napi_ok); - if (valuetype0 != napi_string || valuetype1 != napi_number) { + napi_valuetype valuetype2; + status = napi_typeof(env, args[2], &valuetype2); + assert(status == napi_ok); + + if (valuetype0 != napi_string || valuetype1 != napi_string || valuetype2 != napi_number) { napi_throw_type_error( - env, NULL, "Wrong argument types, expected (string, number, object?)"); + env, NULL, "Wrong argument types, expected (string, string, number, object?)"); return NULL; } @@ -163,29 +167,36 @@ static napi_value Install(napi_env env, napi_callback_info info) { return NULL; } + char *lastRunInfoFilePath = read_string_value(env, args[1], false); + if (!lastRunInfoFilePath) { + free(filepath); + return NULL; + } + double max_crumbs; - status = napi_get_value_double(env, args[1], &max_crumbs); + status = napi_get_value_double(env, args[2], &max_crumbs); assert(status == napi_ok); - if (argc > 2) { - napi_valuetype valuetype2; - status = napi_typeof(env, args[2], &valuetype2); + if (argc > 3) { + napi_valuetype valuetype3; + status = napi_typeof(env, args[3], &valuetype3); assert(status == napi_ok); - if (valuetype2 == napi_object) { - char *state = read_string_value(env, json_stringify(env, args[2]), true); - becsp_install(filepath, max_crumbs, state); + if (valuetype3 == napi_object) { + char *state = read_string_value(env, json_stringify(env, args[3]), true); + becsp_install(filepath, lastRunInfoFilePath, max_crumbs, state); free(state); } else { napi_throw_type_error( env, NULL, - "Wrong argument types, expected (string, number, object?)"); + "Wrong argument types, expected (string, string, number, object?)"); } } else { - becsp_install(filepath, max_crumbs, NULL); + becsp_install(filepath, lastRunInfoFilePath, max_crumbs, NULL); } free(filepath); + free(lastRunInfoFilePath); return NULL; } @@ -379,6 +390,29 @@ static napi_value PersistState(napi_env env, napi_callback_info info) { return NULL; } +static napi_value SetLastRunInfo(napi_env env, napi_callback_info info) { + char *lastRunInfo; + size_t argc = 1; + napi_value args[1]; + napi_status status = napi_get_cb_info(env, info, &argc, args, NULL, NULL); + assert(status == napi_ok); + + if (argc < 1) { + napi_throw_type_error(env, NULL, "Wrong number of arguments, expected 1"); + return NULL; + } + + lastRunInfo = read_string_value(env, args[0], false); + + becsp_set_last_run_info(lastRunInfo); + return NULL; +} + +static napi_value PersistLastRunInfo(napi_env env, napi_callback_info info) { + bescp_persist_last_run_info_if_required(); + return NULL; +} + #define DECLARE_NAPI_METHOD(name, func) \ (napi_property_descriptor) { name, 0, func, 0, 0, 0, napi_default, 0 } @@ -419,6 +453,14 @@ napi_value Init(napi_env env, napi_value exports) { status = napi_define_properties(env, exports, 1, &desc); assert(status == napi_ok); + desc = DECLARE_NAPI_METHOD("setLastRunInfo", SetLastRunInfo); + status = napi_define_properties(env, exports, 1, &desc); + assert(status == napi_ok); + + desc = DECLARE_NAPI_METHOD("persistLastRunInfo", PersistLastRunInfo); + status = napi_define_properties(env, exports, 1, &desc); + assert(status == napi_ok); + desc = DECLARE_NAPI_METHOD("uninstall", Uninstall); status = napi_define_properties(env, exports, 1, &desc); assert(status == napi_ok); diff --git a/packages/plugin-electron-client-state-persistence/src/bugsnag_electron_client_state_persistence.c b/packages/plugin-electron-client-state-persistence/src/bugsnag_electron_client_state_persistence.c index 50af6935b..556cc66ed 100644 --- a/packages/plugin-electron-client-state-persistence/src/bugsnag_electron_client_state_persistence.c +++ b/packages/plugin-electron-client-state-persistence/src/bugsnag_electron_client_state_persistence.c @@ -24,6 +24,12 @@ typedef struct { char *serialized_data; // Length of serialized data in bytes size_t serialized_data_len; + // Path to the serialized file on disk + char *last_run_info_file_path; + // The cached serialized lastRunInfo JSON object + char *last_run_info_data; + // Length of lastRunInfo serialized data in bytes + size_t last_run_info_data_len; // A lock for synchronizing access to the JSON object mtx_t lock; } becsp_context; @@ -46,6 +52,7 @@ static const char *const keypath_user_email = "user.email"; static void handle_crash(int context) { becsp_persist_to_disk(); + bescp_persist_last_run_info_if_required(); // Uninstall handlers becsp_crash_handler_uninstall(); // Invoke previous handler @@ -96,7 +103,9 @@ static JSON_Value *initialize_context(const char *state) { return json_value_init_object(); } -void becsp_install(const char *save_file_path, uint8_t max_crumbs, +void becsp_install(const char *save_file_path, + const char *last_run_info_file_path, + uint8_t max_crumbs, const char *state) { if (g_context.data != NULL) { return; @@ -105,6 +114,8 @@ void becsp_install(const char *save_file_path, uint8_t max_crumbs, mtx_init(&g_context.lock, mtx_plain); // Cache the save path g_context.save_file_path = strdup(save_file_path); + // Cache the lastRunInfo save path + g_context.last_run_info_file_path = strdup(last_run_info_file_path); // Set breadcrumb limit g_context.max_crumbs = max_crumbs; @@ -125,6 +136,10 @@ void becsp_uninstall() { } becsp_crash_handler_uninstall(); free((void *)g_context.save_file_path); + free((void *)g_context.last_run_info_file_path); + if(g_context.last_run_info_data_len) { + free((void *)g_context.last_run_info_data); + } free(g_context.serialized_data); json_value_free(g_context.data); @@ -132,6 +147,9 @@ void becsp_uninstall() { g_context.data = NULL; g_context.save_file_path = NULL; g_context.serialized_data = NULL; + g_context.last_run_info_data_len = 0; + g_context.last_run_info_data = NULL; + g_context.last_run_info_file_path = NULL; } BECSP_STATUS becsp_add_breadcrumb(const char *val) { @@ -362,6 +380,25 @@ BECSP_STATUS becsp_set_user(const char *id, const char *email, const char *name) return BECSP_STATUS_SUCCESS; } +BECSP_STATUS becsp_set_last_run_info(const char *encoded_json) { + if (!g_context.data) { + return BECSP_STATUS_NOT_INSTALLED; + } + context_lock(); + + // release the previously cached lastRunInfo string (if there is one) + if(g_context.last_run_info_data) { + g_context.last_run_info_data_len = 0; + free((void*)g_context.last_run_info_data); + } + + g_context.last_run_info_data = encoded_json; + g_context.last_run_info_data_len = strlen(encoded_json); + + context_unlock(); + return BECSP_STATUS_SUCCESS; +} + // Must be async-signal-safe BECSP_STATUS becsp_persist_to_disk() { if (!g_context.save_file_path) { @@ -380,3 +417,25 @@ BECSP_STATUS becsp_persist_to_disk() { return len == g_context.serialized_data_len ? BECSP_STATUS_SUCCESS : BECSP_STATUS_UNKNOWN_FAILURE; } + +// Must be async-signal-safe - save the lastRunInfo set for a crash +BECSP_STATUS bescp_persist_last_run_info_if_required() { + if(!g_context.last_run_info_file_path) { + return BECSP_STATUS_NOT_INSTALLED; + } + + if(!g_context.last_run_info_data || g_context.last_run_info_data_len == 0) { + return BECSP_STATUS_SUCCESS; + } + + int fd = open(g_context.last_run_info_file_path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd == -1) { + return BECSP_STATUS_UNKNOWN_FAILURE; + } + // Write last_run_info + write(fd, g_context.last_run_info_data, g_context.last_run_info_data_len); + // Close last_run_info file + close(fd); + + return BECSP_STATUS_SUCCESS; +} diff --git a/packages/plugin-electron-client-state-persistence/src/bugsnag_electron_client_state_persistence.h b/packages/plugin-electron-client-state-persistence/src/bugsnag_electron_client_state_persistence.h index 0f604312f..e17e36604 100644 --- a/packages/plugin-electron-client-state-persistence/src/bugsnag_electron_client_state_persistence.h +++ b/packages/plugin-electron-client-state-persistence/src/bugsnag_electron_client_state_persistence.h @@ -37,12 +37,15 @@ typedef enum { /** * Install a handler which will write to disk in the event of a crash * - * @param save_file_path The path to write in the event of a crash. The - * enclosing directory must exist. - * @param max_crumbs The maximum number of breadcrumbs to save - * @param state Stringified JSON of the initial cached state + * @param save_file_path The path to write in the event of a crash. The + * enclosing directory must exist. + * @param last_run_info_file_path The path to write the lastRunInfo to in the event of a crash. + * @param max_crumbs The maximum number of breadcrumbs to save + * @param state Stringified JSON of the initial cached state */ -void becsp_install(const char *save_file_path, uint8_t max_crumbs, +void becsp_install(const char *save_file_path, + const char *last_run_info_file_path, + uint8_t max_crumbs, const char *state); void becsp_uninstall(void); @@ -106,10 +109,22 @@ BECSP_STATUS becsp_set_device(const char *value); */ BECSP_STATUS becsp_set_session(const char *value); +/** + * Set the value of the lastRunInfo field + * + * @param encoded_json the JSON encoded content of the lastRunInfo object + */ +BECSP_STATUS becsp_set_last_run_info(const char *encoded_json); + /** * Write cached event context to disk */ BECSP_STATUS becsp_persist_to_disk(void); + +/** + * Write the lastRunInfo blob to disk + */ +BECSP_STATUS bescp_persist_last_run_info_if_required(void); #ifdef __cplusplus } #endif diff --git a/packages/plugin-electron-client-state-persistence/test/error-handling.test.ts b/packages/plugin-electron-client-state-persistence/test/error-handling.test.ts index 325cb152c..2b0f72380 100644 --- a/packages/plugin-electron-client-state-persistence/test/error-handling.test.ts +++ b/packages/plugin-electron-client-state-persistence/test/error-handling.test.ts @@ -1,7 +1,7 @@ import { NativeClient } from '..' describe('handling poor inputs', () => { - beforeAll(() => NativeClient.install('/tmp/file.json', 10)) + beforeAll(() => NativeClient.install('/tmp/file.json', '/tmp/last-run-info.json', 10)) afterAll(() => NativeClient.uninstall()) diff --git a/packages/plugin-electron-client-state-persistence/test/persistence.test.ts b/packages/plugin-electron-client-state-persistence/test/persistence.test.ts index 8c629f5ba..ade5fc94d 100644 --- a/packages/plugin-electron-client-state-persistence/test/persistence.test.ts +++ b/packages/plugin-electron-client-state-persistence/test/persistence.test.ts @@ -7,6 +7,7 @@ const { mkdtemp, readFile, rmdir } = promises describe('persisting changes to disk', () => { let tempdir: string = '' let filepath: string = '' + let lastRunInfoFilePath: string = '' const readTempFile = async () => { const contents = await readFile(filepath) @@ -16,6 +17,7 @@ describe('persisting changes to disk', () => { beforeEach(async () => { tempdir = await mkdtemp('client-sync-') filepath = join(tempdir, 'output.json') + lastRunInfoFilePath = join(tempdir, 'last-run-info.json') }) afterEach(async (done) => { @@ -25,7 +27,7 @@ describe('persisting changes to disk', () => { }) it('sets context', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.updateContext('silverfish') NativeClient.persistState() const state = await readTempFile() @@ -34,7 +36,7 @@ describe('persisting changes to disk', () => { }) it('sets user fields', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.updateUser('456', 'jo@example.com', 'jo') NativeClient.persistState() const state = await readTempFile() @@ -43,7 +45,7 @@ describe('persisting changes to disk', () => { }) it('clears user fields', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.updateUser('456', 'jo@example.com', 'jo') NativeClient.updateUser('456', 'jo@example.com', null) NativeClient.persistState() @@ -53,7 +55,7 @@ describe('persisting changes to disk', () => { }) it('clears context', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.updateContext('silverfish') NativeClient.updateContext(null) NativeClient.persistState() @@ -63,7 +65,7 @@ describe('persisting changes to disk', () => { }) it('adds breadcrumbs', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.leaveBreadcrumb({ name: 'launch app' }) NativeClient.leaveBreadcrumb({ name: 'click start' }) NativeClient.leaveBreadcrumb({ name: 'click pause' }) @@ -84,7 +86,7 @@ describe('persisting changes to disk', () => { }) it('sets metadata', async () => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.updateMetadata({ terrain: { spawn: 'desert', current: 'cave' }, location: { x: 4, y: 12 } @@ -99,7 +101,7 @@ describe('persisting changes to disk', () => { }) it('set metadata tab contents', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.updateMetadata('terrain', { spawn: 'desert', current: 'cave' }) NativeClient.persistState() @@ -114,7 +116,7 @@ describe('persisting changes to disk', () => { }) it('clears metadata tab', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.updateMetadata('terrain', { spawn: 'desert', current: 'cave' }) NativeClient.updateMetadata('device', { size: 256 }) NativeClient.updateMetadata('terrain') @@ -129,7 +131,7 @@ describe('persisting changes to disk', () => { }) it('sets session', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.setSession({ id: '9f65c975-8155-456f-91e5-c4c4b3db0555', events: { handled: 1, unhandled: 0 }, @@ -154,7 +156,7 @@ describe('persisting changes to disk', () => { }) it('has no session by default', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.persistState() const state = await readTempFile() expect(state.session).toBeUndefined() @@ -163,7 +165,7 @@ describe('persisting changes to disk', () => { }) it('sets app info', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.setApp({ releaseStage: 'beta1', version: '1.0.22' @@ -180,7 +182,7 @@ describe('persisting changes to disk', () => { }) it('sets device info', async (done) => { - NativeClient.install(filepath, 5) + NativeClient.install(filepath, lastRunInfoFilePath, 5) NativeClient.setDevice({ online: true, osName: 'beOS', @@ -201,7 +203,7 @@ describe('persisting changes to disk', () => { }) it('initializes with provided state', async () => { - NativeClient.install(filepath, 5, { + NativeClient.install(filepath, lastRunInfoFilePath, 5, { metadata: { colors: { main: ['yellow', 'green'] } }, context: 'color picker view', title: 'double double, toil and …' @@ -217,7 +219,7 @@ describe('persisting changes to disk', () => { }) it('overrides initial state with new values', async () => { - NativeClient.install(filepath, 5, { + NativeClient.install(filepath, lastRunInfoFilePath, 5, { metadata: { colors: { main: ['yellow', 'green'] } }, context: 'color picker view', title: 'double double, toil and …' @@ -234,7 +236,7 @@ describe('persisting changes to disk', () => { }) it('gracefully handles invalid initial breadcrumb state', async () => { - NativeClient.install(filepath, 5, { + NativeClient.install(filepath, lastRunInfoFilePath, 5, { metadata: { colors: { main: ['yellow', 'green'] } }, context: 'color picker view', breadcrumbs: 'oy' @@ -249,7 +251,7 @@ describe('persisting changes to disk', () => { }) it('gracefully handles invalid initial context', async () => { - NativeClient.install(filepath, 5, { + NativeClient.install(filepath, lastRunInfoFilePath, 5, { metadata: { colors: { main: ['yellow', 'green'] } }, context: 20 }) @@ -262,7 +264,7 @@ describe('persisting changes to disk', () => { }) it('gracefully handles invalid initial metadata', async () => { - NativeClient.install(filepath, 5, { + NativeClient.install(filepath, lastRunInfoFilePath, 5, { metadata: 'things', context: 'side' }) @@ -273,7 +275,7 @@ describe('persisting changes to disk', () => { }) it('gracefully handles invalid initial user info', async () => { - NativeClient.install(filepath, 5, { + NativeClient.install(filepath, lastRunInfoFilePath, 5, { metadata: { colors: { main: ['yellow', 'green'] } }, user: ['foo'] }) @@ -284,4 +286,16 @@ describe('persisting changes to disk', () => { metadata: { colors: { main: ['yellow', 'green'] } } }) }) + + it('saves lastRunInfo', async () => { + const runInfo = { crashed: false, crashedDuringLaunch: false, consecutiveLaunchCrashes: 0 } + + NativeClient.install(filepath, lastRunInfoFilePath, 5) + + NativeClient.setLastRunInfo(JSON.stringify(runInfo)) + NativeClient.persistLastRunInfo() + + const loadedRunInfo = JSON.parse(await readFile(lastRunInfoFilePath, 'utf8')) + expect(runInfo).toEqual(loadedRunInfo) + }) }) diff --git a/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js b/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js index 8927ec2c5..1f728743e 100644 --- a/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js +++ b/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js @@ -6,23 +6,25 @@ const NetworkStatus = require('@bugsnag/electron-network-status') const isEnabledFor = client => client._config.autoDetectErrors && client._config.enabledErrorTypes.nativeCrashes -module.exports = (app, net, filestore, nativeClient) => ({ +module.exports = (app, net, filestore, NativeClient) => ({ name: 'deliverMinidumps', load: (client) => { if (!isEnabledFor(client)) { return } + const appRunMetadata = filestore.getAppRunMetadata() + // make sure that the Electron CrashReporter is configured - const metadata = filestore.createAppRunMetadata() crashReporter.start({ submitURL: '', uploadToServer: false, - extra: metadata + extra: appRunMetadata }) - nativeClient.install( - filestore.getEventInfoPath(metadata.bugsnag_crash_id), + NativeClient.install( + filestore.getEventInfoPath(appRunMetadata.bugsnag_crash_id), + filestore.getPaths().lastRunInfo, client._config.maxBreadcrumbs ) diff --git a/packages/plugin-electron-deliver-minidumps/test/deliver-minidumps.test.ts b/packages/plugin-electron-deliver-minidumps/test/deliver-minidumps.test.ts index d1421aff2..5f96ee132 100644 --- a/packages/plugin-electron-deliver-minidumps/test/deliver-minidumps.test.ts +++ b/packages/plugin-electron-deliver-minidumps/test/deliver-minidumps.test.ts @@ -11,8 +11,9 @@ describe('electron-minidump-delivery: load', () => { const net = {} const filestore = { - createAppRunMetadata: () => ({ bugsnag_crash_id: 'abc123' }), - getEventInfoPath: () => 'test-run-info-dir' + getAppRunMetadata: () => ({ bugsnag_crash_id: 'abc123' }), + getEventInfoPath: () => 'test-run-info-dir', + getPaths: () => ({ lastRunInfo: 'last-run-info.json' }) } const nativeClient = { diff --git a/scripts/cppcheck.sh b/scripts/cppcheck.sh index bfec794ba..7434aa383 100755 --- a/scripts/cppcheck.sh +++ b/scripts/cppcheck.sh @@ -19,7 +19,7 @@ SUPPRESSED_ERRORS=(\ --suppress='ConfigurationNotChecked:*/deps/parson/parson.c:1425' \ --suppress='knownConditionTrueFalse:*/deps/parson/parson.c:692' \ --suppress='memleak:*/plugin-electron-client-state-persistence/src/deps/tinycthread/tinycthread.c:620' \ - --suppress='unusedFunction:*/plugin-electron-client-state-persistence/src/api.c:429' \ + --suppress='unusedFunction:*/plugin-electron-client-state-persistence/src/api.c:471' \ --suppress='unusedFunction:*/plugin-electron-app/src/api.c:60') # Shared arguments: diff --git a/test/electron/features/last-run-info.feature b/test/electron/features/last-run-info.feature new file mode 100644 index 000000000..74cb77c31 --- /dev/null +++ b/test/electron/features/last-run-info.feature @@ -0,0 +1,39 @@ +Feature: lastRunInfo + + Scenario: Last run info consecutive crashes on-launch + # Crash the app during launch - twice + Given I launch an app with configuration: + | bugsnag | zero-launch-duration | + And I click "main-process-crash" + And I wait 2 seconds + + Then I launch an app with configuration: + | bugsnag | zero-launch-duration | + And I click "main-process-crash" + And I wait 2 seconds + + Then I launch an app + And I click "last-run-info-breadcrumb" + And I click "main-process-uncaught-exception" + Then the total requests received by the server matches: + | events | 1 | + Then the headers of every event request contains: + | Bugsnag-API-Key | 6425093c6530f554a9897d2d7d38e248 | + | Content-Type | application/json | + Then the contents of an event request matches "launch-info/consecutive-launch-crashes.json" + + Scenario: Last run info after crash + Given I launch an app with configuration: + | bugsnag | zero-launch-duration | + And I click "mark-launch-complete" + And I click "main-process-crash" + And I wait 2 seconds + Then I launch an app + And I click "last-run-info-breadcrumb" + And I click "main-process-uncaught-exception" + Then the total requests received by the server matches: + | events | 1 | + Then the headers of every event request contains: + | Bugsnag-API-Key | 6425093c6530f554a9897d2d7d38e248 | + | Content-Type | application/json | + Then the contents of an event request matches "launch-info/crashed.json" diff --git a/test/electron/features/support/steps/request-steps.js b/test/electron/features/support/steps/request-steps.js index f38a83311..d963c5a6a 100644 --- a/test/electron/features/support/steps/request-steps.js +++ b/test/electron/features/support/steps/request-steps.js @@ -167,3 +167,7 @@ Then('the event metadata {string} is less than {int}', async (field, max) => { expect(metadata[section][key]).toBeDefined() expect(metadata[section][key]).toBeLessThan(max) }) + +Then('I wait {int} seconds', delay => { + return new Promise(resolve => setTimeout(resolve, delay * 1000)) +}) diff --git a/test/electron/fixtures/app/index.html b/test/electron/fixtures/app/index.html index 243dec372..fd0c9da19 100644 --- a/test/electron/fixtures/app/index.html +++ b/test/electron/fixtures/app/index.html @@ -22,6 +22,7 @@

actions