Webpackを使っているElectronアプリにSentry導入してみた

ElectronアプリにSentryを導入し、エラーの詳細を把握できるようにしてみました。

Webpackの設定手順が煩雑なので実際のプロジェクトでは electron-webpack (Doc) や electron-react-boilerplate (Doc)などのテンプレートプロジェクトを元にプロジェクトを作っていますが、今回は新規にアプリケーションを作ってSentryでエラーを補足できるまでの手順の詳細を書いてみます。

手順の詳細を把握することで仕組みが理解でき、既に動いているElectronアプリに関しても導入が容易になるかと思います。 Sentryでエラーを補足するためにsource mapを送る方法がいくつかあるのですが、一番手間が少ないのでWebpack Pluginを使った方法をご紹介します。

サンプルプロジェクト

今回作ったサンプルプロジェクトをgithubにコードを上げておいたので、よかったら参考にしてみてください。 github.com

環境

  • Ubuntu 18.04 LTS
  • sentry/electron@1.3.1
  • electron@9.0.5
  • @webpack-cli/init@0.2.2

webpack4 からは設定ファイル webpack.config.js が必要なくなったという理由から、 webpack-cli/init@0.3 では自動的に生成しなくなったので、バージョン 0.2.2 を使用しました。

ドキュメント

Sentry

導入するにあたって事前にSentryの以下のドキュメントを読み、オプション等を把握しておきました。

Electron crash-reporter

また、Sentry SDK( @sentry/electron )の内部で使っているElectronのcrash-reporterのドキュメントも合わせて読んでおいて

  • crash-reporterが元々どういう機能のもの
  • Sentry側がどこ設定を簡略化してくれるのか

を把握でき、何かトラブルが発生した時に対処できるようになると思いますので、一読しておくと良いと思います。

事前準備

Sentryのアカウントなければ、取得しておきます。

Sentryのプロジェクト準備

プラットフォームにElectronを選択し、Sentryのプロジェクトを作っておきます。

f:id:moritamorie:20200621153954p:plain

以下のように開発プロジェクト側の設定手順が画面が表示されるので、基本的には同じ事を設定すればOKです。最新(2020年7月現在)のバージョンでは注意点があるので、追って解説していきます。 f:id:moritamorie:20200621154026p:plain

プロジェクトの作成

設定ファイル(package.json, webpack.config.jsを作成)

プロジェクトをディレクトリを作ります。

$ mkdir electron-sentry
$ cd electron-sentry

package.json の初期ファイルを作ります。

$ npm init -y

Webpackの初期ファイルを作ります。

$ npm i @webpack-cli/init@0.2.2 --save-dev
$ npx webpack-cli init

# Electronはメインプロセスのエントリファイルが1つのためNoにする
? Will your application have multiple bundles? No

# mainプロセスのjsファイルをmainにしたいのでmainにする
? Which will be your application entry point? main

# Webpackビルド後のファイルの出力先をdistにする。outputとかフォルダ変えても良い。
? In which folder do you want to store your generated bundles? 'dist'

# Typescriptでも良いがES6にする。今回CSSは使わないけど、SASSにする。
? Will you use one of the below JS solutions? ES6
? Will you use one of the below CSS solutions? SASS
? If you want to bundle your CSS files, what will you name the bundle? (press en
ter to skip) main
 conflict package.json

# package.jsonを上書き
? Overwrite package.json? overwrite
    force package.json
   create .babelrc
   create main.js
   create README.md

Electron、sentry関連、HTMLバンドルのためのパッケージ追加

各種パッケージ追加

$ npm i electron @sentry/electron @sentry/webpack-plugin html-webpack-plugin --save-dev

メインプロセスのjs(main.js)、レンダラプロセスの画面(index.html)・js(renderer.js)を作る

シンプルになコードを作ってみました。(全てプロジェクト直下に追加し、既に存在するmain.jsは上書きしました。)

注意点は、以下の3点です。

  • Sentry SDKのオプションdsnには、上記のSentryのプロジェクト準備で表示されたURLを入れます。
  • Sentry SDK のinit関数をメインプロセスとレンダラプロセスで分けます
    • メインプロセスの場合 @sentry/electron/dist/main から読む
    • レンダラプロセスの場合は @sentry/electron/dist/renderer 読み込む
    • ※そうしないとTypeError: mod.require is not a functionというエラーが発生。参考issue
  • init関数はメインプロセスの場合、app.on("ready")より前、できるだけ早いタイミングで呼ぶと良いようです

【メインプロセスのjsファイル(main.js)】

# main.js
const { app, BrowserWindow } = require("electron")
import * as path from 'path';
import * as url from 'url';
import { init } from '@sentry/electron/dist/main'
import * as Sentry from '@sentry/electron'
let win;

// Sentry SDKのinit。この後メインプロセス内でエラーを補足するとSentryに通知。
init({dsn: 'https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@oxxxxxx.ingest.sentry.io/xxxxxxx'})

// 画面表示(レンダラプロセス起動)
function createWindow() {
  win = new BrowserWindow({ 
    webPreferences: {
      nodeIntegration: true
    }, 
    width: 800, 
    height: 600, 
    webSecurity: false 
  });
  win.loadURL(
    url.format({
      pathname: path.join(__dirname, 'index.html'),
      protocol: 'file:',
      slashes: true
    })
  );
  win.on("closed", () => { win = null; });
}

// readyのライフサイクルでレンダラプロセスを表示
app.on("ready", createWindow);

// ウィンドウを全部閉じたらappを終了
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

// ウィンドウアクティブ時にwinオブジェクトがなければ画面表示
app.on("activate", () => {
  if (win === null) {
    createWindow();
  }
})

【レンダラプロセスの画面ファイル(index.html)】

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>electron-sentry | Electronでsentry通知するサンプル</title>
</head>
<body>
  <h1>レンダラプロセスの画面</h1>

  <!-- レンダラプロセスのJSを読み込む -->
  <script src="dist/renderer.js"></script>
</body>
</html>

【レンダラプロセスのjsファイル(renderer.js)】

import { remote } from 'electron'

const { app } = remote
import { init } from '@sentry/electron/dist/renderer'
import * as Sentry from '@sentry/electron'

// Sentry SDKのinit。この後レンダラプロセス内でエラーを補足するとSentryに通知。レンダラプロセスが複数ある場合、その全てでinitする必要があります。
init({dsn: 'https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@oxxxxxx.ingest.sentry.io/xxxxxxx'})

document.addEventListener("DOMContentLoaded", () => {
  // 定義していない関数を呼び出して、エラーを発生させる
  myUndefinedFunction()
})

Webpack設定ファイル webpack.config.js の編集

ポイントとしては

  • Webpackの出力先(output)
    • アプリケーション実行に必要なファイルがdist内のみに収まるようにHtmlWebpackPluginでレンダラ画面のhtmlもコピー
  • SentryCliPluginの引数
    • release: package.jsonのname + version(例: electron-sentry1.0.0)になるように
      • エラーがどのバージョンをで発生したのかを把握できるように
    • include

となるようにしてみました。

const SentryCliPlugin = require('@sentry/webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path');
const webpack = require('webpack')

const package = require('./package.json');
const sentryRelease = `${package.name}${package.version}`;

const main = {
  mode: 'development',
  devtool: 'source-map',
  target: 'electron-main',
  node: {
    __dirname: false,
    __filename: false,
  },
  entry: './main.js',
  output: {
    path: __dirname + '/dist',
    filename: 'main.js'
  },
  plugins: [
    new SentryCliPlugin({
      release: sentryRelease,
      include: './dist'
    })
  ] 
}

const renderer = {
  mode: 'development',
  devtool: 'source-map',
  target: 'electron-renderer',
  node: {
    __dirname: false,
    __filename: false,
  },
  entry: {
    renderer: './renderer.js',
  },
  output: {
    path: __dirname + '/dist',
    filename: 'renderer.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, './index.html'),
    }),
    new SentryCliPlugin({
      release: sentryRelease,
      include: './dist'
    })
  ]
}

module.exports = [ main, renderer ]

npm設定ファイル package.json の編集

上記で生成したpackage.json のmainとscriptsを変更します。

electron がWebpackで生成した dist/main.js を実行するようにしておきます。

  // 〜〜〜
  "main": "dist/main.js",
  "scripts": {
    "start": "electron .",
  }
  // 〜〜〜

.envファイルを追加

SentryCliPluginはWebpackのバンドル時に、自動的に.env等の設定があればそれを参照し、distに出力されたファイルをsentryに送信してくれるので、.envファイルをプロジェクト配下に作っておきます。

# .env
SENTRY_DSN= https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@oxxxxxx.ingest.sentry.io/xxxxxxx
SENTRY_ORG=[org name]
SENTRY_PROJECT=electron-sentry
SENTRY_AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

sentry cliの認証に関しての詳細は、以下のドキュメントに詳しく書いていました。 Configuration and Authentication - Docs

また、AUTH_TOKENは、Sentryのアカウント毎に以下のURLから生成することができます。 https://sentry.io/settings/account/api/auth-tokens/

Sentryでエラーの捕捉を確認

Electronアプリ実行してエラーを確認する前に

  • npmパッケージインストール
  • distディレクトリに実行するコードをwebpackで生成
  • Sentryにsource mapを送る

をしておきます。 以下を実行してください

$ npm install
$ npx webpack

> Found 4 release files
> Analyzing 4 sources
> Rewriting sources
> Adding source map references
> Bundled 4 files for upload
> Uploading release files...

> Source Map Upload Report
>   Scripts
>     ~/main.js
>     ~/renderer.js
>   Source Maps
>     ~/main.js.map
>     ~/renderer.js.map
> Uploaded release files to Sentry
> File upload complete

Electronアプリを起動すると

$ npm start

画面表示後に、chrome developer toolsを開き(ショートカットはCtrl+Shift+i)、consoleにエラーが出ていることが確認できます。

f:id:moritamorie:20200705231618p:plain

Sentryのコンソールを開いてみても、エラーが発生していることが確認できます。ソースコードが読めているのでsource mapsもちゃんと送れていそうです。 f:id:moritamorie:20200706001142p:plain

また、Webpackでバンドルした際にSentryにsource mapが送られていることも確認しておきます。ReleasesのArtifactsから確認できます。 f:id:moritamorie:20200706010024p:plain

うまくソースコードが表示できない場合

エラーは通知されるのに、Sentry上でうまくソースコードが表示されない時は大体

  • 通知されたコードのパス
  • SentryのReleasesのArtifactsのファイルのパス

がずれていることが原因です。そういう時は、urlPrefixをつけて相対パスを指定してあげたりすれば、表示されるようになります。

new SentryCliPlugin({
  include: './js',
  release: sentryRelease,
  urlPrefix: '~/js'
}),