2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Blazor WebAssembly で Raygun を使って予期されない例外発生を記録・通知する

Posted at

Raygun とは

Raygun(レイガン)は、ソフトウェアのエラー、クラッシュ、パフォーマンスの問題をリアルタイムで監視・診断し、開発者がバグを迅速に特定・修正できるようにするサービスです。

Raygun は様々な言語・ライブラリ・フレームワークに対応しており、Blazor WebAssembly もサポート対象となっています。

先日、こんな記事を投稿しました。

上記記事では Sentry というプロダクトおよびその SaaS を使って、Blazor WebAssembly アプリケーション上で発生した予期せぬ例外を捕捉、記録、通知し、Sentry の Web UI 上でその例外の詳細やユーザー行動を把握する、という方法について説明しました。

そして今回は、Sentry ではなく、Raygun を使って同じように、Blazor WebAssembly アプリケーション上の予期せぬ例外の捕捉・記録・通知を試してみます。

Raygun 上でのプロジェクトの作成

Raygun https://raygun.com/ にサインアップすると、まずは "アプリケーション" の新規作成が求められます。アプリケーション名を決めて入力し、他の項目は既定値のままでよいので、[Continue] ボタンをクリックします。

Raygun のアプリケーション新規作成ページ

すると、クラッシュレポートのセットアップについての画面に進みます。

ここで、監視対象のアプリケーションのプログラミング言語 (フレームワーク) を選択するドロップダウンリストが表示されます。選択肢にちゃんと "Blazor" があるので、"Blazor" を選択します。すると、続く記載が、Blazor アプリケーションに Raygun を組み込む手順が表示されます。もちろん、Blazor WebAssembly についてもちゃんと手順が記載されていますので、下へスクロールしていって、手順を読み進めつつ、手元の Blazor WebAssembly アプリケーションプロジェクトにその手順を適用します。

Raygun のクラッシュレポートのセットアップページ

以下にその手順を再録します。

Blazor WebAssembly アプリケーションプロジェクトへの組み込み

NuGet パッケージの追加

Raygun.Blazor および Raygun.Blazor.WebAssembly NuGet パッケージを Blazor WebAssembly アプリケーションプロジェクトに追加します。

*.csproj
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1" />
    <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all" />
+   <PackageReference Include="Raygun.Blazor" Version="2.0.0" />
+   <PackageReference Include="Raygun.Blazor.WebAssembly" Version="2.0.0" />
  </ItemGroup>

</Project>

API キーの構成

Blazor WebAssembly アプリケーションプロジェクト内の wwwroot フォルダ内に appsettings.json ファイルを新規に作成し、以下のように Raygun の API キーを書き込んでおきます。

wwwroot/sppsettings.json
{
  "Raygun": {
    "ApiKey": "*** ここに API キーを記入 ***"
  }
}

API キーは下図のように、新規アプリケーション作成時のクラッシュレポートセットアップのページに記載されているので、これを転記します。

Raygun のクラッシュレポートセットアップのページに掲載されている API キー

ちなみに API キーは、Raygun のアプリケーションの設定 - 一般のページから、随時確認することが可能です。

Raygun のアプリケーション設定 - 一般に表示されている API キー

サービスや DI の構成

Blazor WebAssembly プロジェクトの Program.cs 内で、Raygun のサービスや DI コンテナの構成を行なう、UseRaygunBlazor() 拡張メソッド呼び出しを追加します。

Program.cs
  using BlazorApp1;
  using BlazorApp1.Services;
  using Microsoft.AspNetCore.Components.Web;
  using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+ using Raygun.Blazor.WebAssembly.Extensions;

  var builder = WebAssemblyHostBuilder.CreateDefault(args);
  builder.RootComponents.Add<App>("#app");
  builder.RootComponents.Add<HeadOutlet>("head::after");

  builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
  builder.Services.AddScoped<CounterService>();

+ builder.UseRaygunBlazor();

  await builder.Build().RunAsync();

例外を捕捉するエラーバウンダリーを追加

次に _Imports.razor に以下の3行を追加し、これら名前空間を開けておきます。

_Imports.razor
  ...
+ @using Raygun.Blazor
+ @using Raygun.Blazor.Models
+ @using Raygun.Blazor.WebAssembly.Controls

その上で、App.razor 内のマークアップ全体を、<RaygunErrorBoundary></RaygunErrorBoundary> で囲みます。どうやら、どこでも捕捉されなかった例外を最終的にこの Raygun によるエラーバウンダリーによって捕捉する模様です。

App.razor
+ <RaygunErrorBoundary>
    <Router AppAssembly="@typeof(App).Assembly"
            NotFoundPage="typeof(Pages.NotFound)">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
    </Router>
+ </RaygunErrorBoundary>

Raygun クライアントの初期化

さらに続けて、App.razor 内に RaygunBlazorClient オブジェクトを DI コンテナから注入し、その InitializeAsync() メソッドを、OnAfterRenderAsync() のタイミングでいちどだけ呼び出し、Raygun クライアントの初期化を済ませておきます。

App.razor
+ @inject RaygunBlazorClient raygunClient;

  <RaygunErrorBoundary>
    <Router ... >
      ...
    </Router>
  </RaygunErrorBoundary>

+ @code {
+   protected override async Task OnAfterRenderAsync(bool firstRender)
+   {
+     if (firstRender) {
+       await raygunClient.InitializeAsync();
+     }
+   }
+ }

動作確認

動作確認のために、わざと捕捉されない例外を発生するようにしてみます。

このサンプルプログラムでは、Counter ページでのボタンクリック回数の計測を DI コンテナに登録するサービスと実装してあります。そこで、この CounterService の実装を変更し、CurrentCount が 3 以上になったら例外をスローするようにしてみます。

Services/CounterService.cs
namespace BlazorApp1.Services;

public class CounterService
{
    public int CurrentCount { get; private set; } = 0;

    public void IncrementCount()
    {
        CurrentCount++;
+       if (CurrentCount >= 3) throw new Exception("これはテストです");
    }
}

この状態でこの Blazor WebAssembly アプリケーションを実行し、Counter ページにて "Click me" ボタンを繰り返しクリックすると、3回目のクリックで例外が発射されます。ただし <RaygunErrorBoundary> によって例外が捕捉されるため、Blazor 標準の黄色いバーでのエラー発生通知は表示されません。

Blazor WebAssembly アプリケーションのカウンターページ

発生した例外は <RaygunErrorBoundary> によって捕捉され、Raygun のサービスエンドポイントに送信されます。すると、Raygun のアプリケーション新規作成におけるクラッシュレポートセットアップ画面での待機から、クラッシュレポートの画面に自動で切り替わります。そこに、いま発生させた例外が記録されているのがわかります。

Raygun のクラッシュレポートページに、例外が記録された様子

このクラッシュレポートページでエラーの行をクリックすると、詳細表示のページに遷移します。例外の型やメッセージ、スタックトレースが記録され、閲覧できることが確認できます。

Raygun のクラッシュレポートの、例外についての詳細

同時に、メールでも、例外発生が通知されます。

Raygun からのクラッシュレポートの通知メール

"Breadcrumb (パンくずリスト)" の利用

Raygun でも、Sentry でやったのと同じように "Breadcrumb (パンくずリスト)" を利用できます。実際にやってみましょう。

App.razor 内の <Router> コンポーネントについて、OnNavigateAsync イベントを捕捉し、ページ遷移時の遷移先 URL パスを、RaygunBlazorClientRecordBreadcrumb() メソッドの呼び出しによって "Breadcrumb (パンくずリスト)" に追加することにします。
実装は以下のとおりです。

App.razor
@inject RaygunBlazorClient raygunClient;

<RaygunErrorBoundary>
    <Router AppAssembly="@typeof(App).Assembly"
            NotFoundPage="typeof(Pages.NotFound)"
+           OnNavigateAsync="OnNavigateAsync">
      ...
    </Router>
</RaygunErrorBoundary>

@code
{
+   private void OnNavigateAsync(NavigationContext args)
+   {
+       // Breadcrumbとしてページ遷移を記録(クラッシュレポートに含まれる)
+       raygunClient.RecordBreadcrumb($"Navigation: /{args.Path}", BreadcrumbType.Navigation);
+   }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await raygunClient.InitializeAsync();
        }
    }
}

RaygunBlazorClient.RecordBreadcrumb() を呼び出した時点では、Raygun への情報の送信は行なわれません。このあと、予期されない例外発生などで Raygun へのエラー情報送信時に、それまでに蓄積された "Breadcrumb (パンくずリスト)" が添えられる仕組みです。

この状態で、"/" → "/weather" → "/counter" とページを渡り歩いた後で "Click me" ボタンのクリック連打を行なうと (そして例外が発生すると)、Raygun のクラッシュレポートページに以下のように "Breadcrumb (パンくずリスト)" のタブが出現し、このタブを開くとどのようなページを辿ってこの例外に至ったのかがわかるようになります。

クラッシュレポートにパンくずリストのタブが増えて、ページ遷移が記録されている様子

エラーバウンダリーを使わずカスタムロガーで例外を記録する

以上が Raygun を使って Blazor WebAssembly アプリケーション上の予期せぬ例外の捕捉・記録を行なう基本的な実装です。しかし以上の実装では、<RaygunErrorBoundary> を使ったエラーバウンダリーによって例外を捕捉する仕組みであるため、既に独自のエラーバウンダリーを実装していたりすると、うまく組み合わせるのが難しい・記録されてほしい例外が記録されない、といった問題が発生します。

そこで <RaygunErrorBoundary> を使わずに、カスタムのロガーを実装して、そこで例外発生を捕捉し、Raygun に送り込む実装を試してみます。

まずは以下のとおり、ILogger を実装したカスタムロガーを実装します。本質的には、ロガーに渡された例外オブジェクトを RaygunBlazorClientRecordExceptionAsync() メソッド呼び出しに渡しているだけです。

RaygunErrorLogger.cs
using Raygun.Blazor;

public class RaygunErrorLogger : ILogger
{
    private readonly string _categoryName;

    private readonly RaygunBlazorClient _raygunClient;

    public RaygunErrorLogger(string categoryName, RaygunBlazorClient raygunClient)
    {
        _categoryName = categoryName;
        _raygunClient = raygunClient;
    }

    public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;

    // ErrorとCriticalのみRaygunに送信
    public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Error;

    public void Log<TState>(
        LogLevel logLevel,
        EventId eventId,
        TState state,
        Exception? exception,
        Func<TState, Exception?, string> formatter)
    {
        if (!IsEnabled(logLevel)) return;

        var message = formatter(state, exception);
        exception ??= new Exception(message);

        // いちばん肝心なのはここ:
        // ロガーに渡された例外情報を Raygun に送信する
        _ = _raygunClient.RecordExceptionAsync(exception, userCustomData: new Dictionary<string, object>
            {
                { "LogLevel", logLevel.ToString() },
                { "Category", _categoryName },
                { "Message", message },
                { "EventId", eventId.ToString() }
            });
    }
}

あとは、このカスタムロガーを生成するプロバイダーを実装して (下記)、

RaygunErrorLoggerProvider.cs
using Raygun.Blazor;

public class RaygunErrorLoggerProvider : ILoggerProvider
{
    private readonly RaygunBlazorClient _raygunClient;

    public RaygunErrorLoggerProvider(RaygunBlazorClient raygunClient)
    {
        _raygunClient = raygunClient;
    }

    public ILogger CreateLogger(string categoryName)
    {
        return new RaygunErrorLogger(categoryName, _raygunClient);
    }

    public void Dispose() { }
}

Program.cs にてこのカスタムロガープロバイダーを組み込みます。

Program.cs
  using BlazorApp1;
  using BlazorApp1.Services;
  using Microsoft.AspNetCore.Components.Web;
  using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
  using Raygun.Blazor.WebAssembly.Extensions;

  var builder = WebAssemblyHostBuilder.CreateDefault(args);
  builder.RootComponents.Add<App>("#app");
  builder.RootComponents.Add<HeadOutlet>("head::after");

  builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
  builder.Services.AddScoped<CounterService>();

+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, RaygunLoggerProvider>());

  builder.UseRaygunBlazor();

  await builder.Build().RunAsync();

カスタムロガーにて例外の捕捉・記録を行なうようにしたので、App.razor 内に組み込んであった <RaygunErrorBoundary> を削除します。

App.razor
@inject RaygunBlazorClient raygunClient;

- <RaygunErrorBoundary>
    <Router AppAssembly="@typeof(App).Assembly"
            NotFoundPage="typeof(Pages.NotFound)"
            OnNavigateAsync="OnNavigateAsync">
      ...
    </Router>
- </RaygunErrorBoundary>

@code
{
    ...
}

以上で、ロガーへの例外記録がそのまま Raygun にも送信されるようになりました。

感想

Raygun も Sentry と同様に Blazor WebAssembly のサポートが手厚く、JavaScript を一切書かせずに組み込めるのが好印象でした。しかしそのいっぽうで、Sentry と比べると、組み込みのための手順がやや多い印象です (RaygunBlazorClientInitializeAsync() メソッドの呼び出しと、<RaygunErrorBoundary> エラーバウンダリーの組み込み)。また、例外の捕捉のために <RaygunErrorBoundary> エラーバウンダリーを使用していたため、既存の例外ハンドリングとの共存・協調に難点を感じました。幸いこの点は、カスタムロガーを実装することで回避できます。しかし少量とはいえ、さらに追加の実装が必要となるとなおさら Sentry SDK の組み込みのしやすさが引き立つように感じました。

もちろん、アプリケーション運用監視は、SDK の組み込みのしやすさだけで語られるべきではありません。しかし「捕捉されない例外を記録・通知する」という要件・観点だけでみると、Sentry と Raygun での機能差はあまり感じられず、Sentry と Raygun の比較なら、どちらかというと Sentry を推したいかも... という感想を持ちました。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?