Raygun とは
Raygun(レイガン)は、ソフトウェアのエラー、クラッシュ、パフォーマンスの問題をリアルタイムで監視・診断し、開発者がバグを迅速に特定・修正できるようにするサービスです。
Raygun は様々な言語・ライブラリ・フレームワークに対応しており、Blazor WebAssembly もサポート対象となっています。
先日、こんな記事を投稿しました。
上記記事では Sentry というプロダクトおよびその SaaS を使って、Blazor WebAssembly アプリケーション上で発生した予期せぬ例外を捕捉、記録、通知し、Sentry の Web UI 上でその例外の詳細やユーザー行動を把握する、という方法について説明しました。
そして今回は、Sentry ではなく、Raygun を使って同じように、Blazor WebAssembly アプリケーション上の予期せぬ例外の捕捉・記録・通知を試してみます。
Raygun 上でのプロジェクトの作成
Raygun https://raygun.com/ にサインアップすると、まずは "アプリケーション" の新規作成が求められます。アプリケーション名を決めて入力し、他の項目は既定値のままでよいので、[Continue] ボタンをクリックします。
すると、クラッシュレポートのセットアップについての画面に進みます。
ここで、監視対象のアプリケーションのプログラミング言語 (フレームワーク) を選択するドロップダウンリストが表示されます。選択肢にちゃんと "Blazor" があるので、"Blazor" を選択します。すると、続く記載が、Blazor アプリケーションに Raygun を組み込む手順が表示されます。もちろん、Blazor WebAssembly についてもちゃんと手順が記載されていますので、下へスクロールしていって、手順を読み進めつつ、手元の Blazor WebAssembly アプリケーションプロジェクトにその手順を適用します。
以下にその手順を再録します。
Blazor WebAssembly アプリケーションプロジェクトへの組み込み
NuGet パッケージの追加
Raygun.Blazor および Raygun.Blazor.WebAssembly NuGet パッケージを Blazor WebAssembly アプリケーションプロジェクトに追加します。
<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 キーを書き込んでおきます。
{
"Raygun": {
"ApiKey": "*** ここに API キーを記入 ***"
}
}
API キーは下図のように、新規アプリケーション作成時のクラッシュレポートセットアップのページに記載されているので、これを転記します。
ちなみに API キーは、Raygun のアプリケーションの設定 - 一般のページから、随時確認することが可能です。
サービスや DI の構成
Blazor WebAssembly プロジェクトの Program.cs 内で、Raygun のサービスや DI コンテナの構成を行なう、UseRaygunBlazor() 拡張メソッド呼び出しを追加します。
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行を追加し、これら名前空間を開けておきます。
...
+ @using Raygun.Blazor
+ @using Raygun.Blazor.Models
+ @using Raygun.Blazor.WebAssembly.Controls
その上で、App.razor 内のマークアップ全体を、<RaygunErrorBoundary> ~ </RaygunErrorBoundary> で囲みます。どうやら、どこでも捕捉されなかった例外を最終的にこの Raygun によるエラーバウンダリーによって捕捉する模様です。
+ <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 クライアントの初期化を済ませておきます。
+ @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 以上になったら例外をスローするようにしてみます。
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 標準の黄色いバーでのエラー発生通知は表示されません。
発生した例外は <RaygunErrorBoundary> によって捕捉され、Raygun のサービスエンドポイントに送信されます。すると、Raygun のアプリケーション新規作成におけるクラッシュレポートセットアップ画面での待機から、クラッシュレポートの画面に自動で切り替わります。そこに、いま発生させた例外が記録されているのがわかります。
このクラッシュレポートページでエラーの行をクリックすると、詳細表示のページに遷移します。例外の型やメッセージ、スタックトレースが記録され、閲覧できることが確認できます。
同時に、メールでも、例外発生が通知されます。
"Breadcrumb (パンくずリスト)" の利用
Raygun でも、Sentry でやったのと同じように "Breadcrumb (パンくずリスト)" を利用できます。実際にやってみましょう。
App.razor 内の <Router> コンポーネントについて、OnNavigateAsync イベントを捕捉し、ページ遷移時の遷移先 URL パスを、RaygunBlazorClient の RecordBreadcrumb() メソッドの呼び出しによって "Breadcrumb (パンくずリスト)" に追加することにします。
実装は以下のとおりです。
@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 を実装したカスタムロガーを実装します。本質的には、ロガーに渡された例外オブジェクトを RaygunBlazorClient の RecordExceptionAsync() メソッド呼び出しに渡しているだけです。
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() }
});
}
}
あとは、このカスタムロガーを生成するプロバイダーを実装して (下記)、
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 にてこのカスタムロガープロバイダーを組み込みます。
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> を削除します。
@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 と比べると、組み込みのための手順がやや多い印象です (RaygunBlazorClient の InitializeAsync() メソッドの呼び出しと、<RaygunErrorBoundary> エラーバウンダリーの組み込み)。また、例外の捕捉のために <RaygunErrorBoundary> エラーバウンダリーを使用していたため、既存の例外ハンドリングとの共存・協調に難点を感じました。幸いこの点は、カスタムロガーを実装することで回避できます。しかし少量とはいえ、さらに追加の実装が必要となるとなおさら Sentry SDK の組み込みのしやすさが引き立つように感じました。
もちろん、アプリケーション運用監視は、SDK の組み込みのしやすさだけで語られるべきではありません。しかし「捕捉されない例外を記録・通知する」という要件・観点だけでみると、Sentry と Raygun での機能差はあまり感じられず、Sentry と Raygun の比較なら、どちらかというと Sentry を推したいかも... という感想を持ちました。








