[Electron] クラッシュレポートの自動送信に対応する

Electronにはアプリがクラッシュした際に自動的にダンプファイルや各種情報を指定サーバへ送信する機能が用意されています。今回は受信サーバも含めて実装してみます。

最終的なファイル

GitHubにこのページのファイルの一覧をアップしました。必要に応じてご利用ください。 github.com

基本的な原理

クライアント

メインプロセス内でcrashReporter.start()を呼ぶだけです。めっちゃ簡単ですね。引数として送信先のURLを指定します。

const { crashReporter } = require('electron')

crashReporter.start({
  submitURL: 'http://localhost:3000/receive'
});

引数

crashReporter.start()の引数はsubmitURLが必須となっていますが、任意で以下の引数を追加することができます。

# 名前 省略時 説明
1 productName app.name アプリ名
2 uploadToServer true サーバに情報を送信するか
3 rateLimit false サーバへの送信を1時間に1回に制限するか(Windows,macOS
4 compress false サーバへ送信する際にgzipで圧縮するか
5 ignoreSystemCrashHandler false メインプロセスで発生したクラッシュをOSに渡さない

compressはtrueが推奨らしくfalseにすると以下の警告が表示されます。将来的に廃止されるようですね。

(electron) Sending uncompressed crash reports is deprecated and will be removed in a future version of Electron. Set { compress: true } to opt-in to the new behavior. Crash reports will be uploaded gzipped, which most crash reporting servers support.

サーバへ任意の値を送信する

crashReporter.start()に「extra」または「globalExtra」を指定すると、その値がそのままサーバへ送信されます。

const { crashReporter } = require('electron')

crashReporter.start({
  submitURL: 'http://localhost:3000/receive',

  // メインプロセスがクラッシュ
  extra:{
    foo1: 'bar1',
    foo2: 'bar2'
  },

  // すべてのプロセスが対象(変更不可)
  globalExtra: {
    hoge1: 'fuga1',
    hoge2: 'fuga2'
  }
});

start()時ではなく任意の箇所で値を追加したい場合には専用のメソッドも用意されています。

crashReporter.addExtraParameter(key, value)

なお次の制限があるのでご注意を。

  • keyの文字列長は39バイト未満
  • valueは20,320バイト未満、指定できるのは文字列のみ

意図的にクラッシュさせる

開発やデバッグなどで強制的にクラッシュさせるためには以下のメソッドを実行します。これが無いと辛いですからねw メインプロセスとレンダラープロセスの両方で利用できます。

process.crash()

受信サーバ

クライアントからサーバへは以下のような形式で通信が行われます。

通信方式

  • POSTメソッド
  • multipart/form-data

クエリー

# 名前 説明
1 upload_file_minidump ダンプファイル(minidump形式)
2 ver Electronのバージョン
3 _varsion package.jsonのバージョン
4 _productName app.nameまたはオプションで指定したアプリ名
5 platform win32, darwin(macOS)など
6 process_type クラッシュが発生した場所。renderer, browser
7 (extraで指定した値) 任意の値。メインプロセスのクラッシュ時のみ
8 (globalExtraで指定した値) 任意の値。

ソースコード

アプリを起動するとメインプロセス側で1秒後にクラッシュします。事前に受信サーバの方を起動してから試してください。

メインプロセス

const { app, BrowserWindow, crashReporter } = require('electron')

// クラッシュリポートを開始
crashReporter.start({
  // 必須
  submitURL: 'http://localhost:3000/receive',

  // 以降は任意
  productName: app.name,   // アプリ名
  uploadToServer: true,    // サーバにアップロードするか
  ignoreSystemCrashHandler: false, // メインプロセスで発生したクラッシュをシステムクラッシュハンドラに転送しない
  rateLimit: false,        // アップロードする回数を1時間に1度にする(Windows,macOS)
  compress: false,         // アップロード時にgzipで圧縮するか
});


let mainWin;

/**
 * ウィンドウを作成する
 */
function createWindow () {
  // ウィンドウを新たに開く
  mainWin = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })

  // ファイルを開く
  mainWin.loadFile('public/index.html')
}

// 初期化が終了
app.whenReady().then(()=>{
  // ウィンドウを新規に作成する
  createWindow()

  // 1秒後にクラッシュ
  setTimeout(()=>{
    process.crash();
  }, 1000)
})


// すべてのウィンドウが閉じられたときの処理
app.on('window-all-closed', () => {
  // macOS以外はアプリを終了する
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

// アプリがアクティブになった時の処理
// (macOSはDocのアイコンがクリックされたとき)
app.on('activate', () => {
  // ウィンドウがすべて閉じられている場合は新しく開く
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

受信サーバ

WebサーバはExpress、データの保存はNeDBで実装した例です。nodeコマンドで起動してください。

$ node serve.js

クライアントでcompressをtrueにしgzipで圧縮して送ると動きません。実装がちょっと面倒なんでサボりましたw

/**
 * crashReorter受信サーバ
 *
 * @version 1.0.0
 * @author M.Katsube
 */

//-----------------------------------
// 定数
//-----------------------------------
const PORT = 3000   // Webサーバのポート番号
const DATA_FILE  = 'crashlist.nedb' // クラッシュ情報のファイル
const UPLOAD_DIR = 'uploads/'       // ダンプファイルの保存先

//-----------------------------------
// モジュール
//-----------------------------------
const Datastore = require('nedb')
const multer = require('multer')
const express = require('express')
const app  = express()

//-----------------------------------
// 初期設定
//-----------------------------------
// NeDB
const db = new Datastore({filename:DATA_FILE, autoload:true});

// express
const upload = multer({dest:UPLOAD_DIR})
app.use(express.urlencoded({extended: true}))

//-----------------------------------
// ルーティング
//-----------------------------------
app.post('/receive', upload.single('upload_file_minidump'), (req, res) =>{
  const data = {
    dumpfile: req.file.filename,
    ...req.body
  }

  db.insert(data, (err, newDoc)=>{
    if (err) throw err;
  });

  res.send('OK')
});

// HTTPサーバを起動する
app.listen(PORT, () => {
  console.log(`listening at http://localhost:${PORT}`)
});

おまけ

ダンプファイルを閲覧する

macOSの場合、Google謹製のBreakpadを利用した方法を以下にまとめました。試していませんがWindowsLinuxでも利用できるようです。 blog.katsubemakito.net

参考ページ