GameWith Developer Blog

GameWith のエンジニア、デザイナーが技術について日々発信していきます。

SentryのCustom Integrationを使ったクリーンな共通イベント処理の実装 #GameWith #TechWith

はじめに

こんにちは。サービス開発部の木村です。

この記事は GameWith アドベントカレンダー2025 15日目の記事です。

GameWithではGMF (GameWith Micro Frontend)と呼ばれているモノレポ開発環境で40個以上のフロントエンドアプリケーションを管理しており、エラートラッキングにはSentryを使用しています。

GMFでは1つのSentryプロジェクトで複数のアプリケーションのエラーの集計を行っており、集計のしやすさを考慮して共通のフィルター機能を実装し、特定のエラーを除外したりイベントに追加情報を付与したりしています。

tech.gamewith.co.jp

Sentryの共通フィルターの実装方法はいくつかありますが、Custom Integrationを活用することで共通のフィルターと複数のアプリケーション毎の固有のフィルターの実装における責任を綺麗に分離できます。GMFでは実際にCustom Integrationで複数のアプリケーションの共通フィルターを実装しています。

今回はSentryのCustom Integrationによる、複数アプリケーションで扱いやすい共通フィルターの実装例を紹介します。

この記事では@sentry/browser@sentry/reactなどを使用した、WebフロントエンドにおけるJavaScriptのエラートラッキングの事例となるため、他のプログラミング言語や実行環境では仕様や挙動が異なる場合があります。

SentryのCustom Integrationとは

SentryのCustom Integrationとは、その名の通りSentryが提供しているIntegration機能を自作する方法の名称です。

Integrationが何かを一言で表すと、「プログラム実行中の問題を検知したり、検知したデータの加工や送信を行うための機能」です。

Sentryでは基本的なIntegrationはデフォルトで有効になっているため普段意識せずに使っている方もいるかもしれませんが、Integrationが無ければ問題の検知ができず、問題発生時の周辺データの収集もできません。

例えば@sentry/browser@sentry/reactでは、グローバルのエラーイベントを収集するためのSentry.globalHandlersIntegrationや、ユーザーの操作ログを記録するためのSentry.breadcrumbsIntegrationなどのIntegrationがデフォルトで有効になっているため、ただSentry.init()を実行するだけでこれらの情報を自動で収集することが可能になっています。

docs.sentry.io

そんなIntegrationを自作することで、プロジェクト固有のタイミングでSentryに独自のイベントを送信したり、イベント送信前に独自のルールでイベントのデータを加工することが可能になります。

次はCustom IntegrationのprocessEventフックを使用して、エラーイベントの除外やタグ追加を行うフィルターを実装する簡単な例です。

import type { Integration } from "@sentry/core";

const myAppsIntegration = (appName: string): Integration => {
  return {
    name: "MyAppsIntegration",
    processEvent(event, hint) {
      // アプリケーション共通で無視したいエラーを除外する
      if (isCommonIgnoreError(hint.originalException)) {
        return null;
      }

      // アプリケーション固有のタグを追加する
      event.tags['apps.name'] = appName;

      return event;
    },
  };
};

Sentry.init({
  integrations: [myAppsIntegration('example-app')],
});

イベント送信前のコールバックであるprocessEvent以外にも、Sentryのライフサイクルに合わせたイベントフックが提供されているので、気になる方は公式ドキュメントをご覧ください。

docs.sentry.io

Custom Integrationによるフィルターの責任の分離

Sentryでエラーのフィルターを行う方法としてbeforeSendignoreErrorsなどのフィルター機能を使う方法が一般的ですが、フィルターの処理を共通化しつつ複数のアプリケーションの固有のフィルターと併用したい場合は、beforeSendignoreErrorsだけで共通化を行うのではなく、Custom Integrationを活用することで共通処理とアプリケーションで責任をクリーンに分離できます。

例えば、複数のアプリケーションで実装されているbeforeSendの処理の一部を共通化するとします。beforeSendはSentryイベントの送信直前でイベント情報の調整を行えるコールバック関数です。

まず、次のように単純にbeforeSendの処理の一部を共通関数として@packages/sentry-configパッケージに切り出す方法について考えてみます。

この方法では各アプリケーション側の実装が冗長になるのと、共通処理をアプリケーション側のロジックに組み込むため、共通パッケージ側の仕様変更時にアプリケーション側でも対応が必要になる場合があります。

// /packages/sentry-config/config.ts
import type { Event, EventHint } from "@sentry/browser";

export const commonBeforeSend = (
  appName: string,
  event: Event,
  hint: EventHint,
): Event | null => {
  if (isCommonIgnoreError(hint.originalException)) {
    return null;
  }

  event.tags['apps.name'] = appName;

  return event;
};


// /apps/example-app/main.ts
import * as Sentry from "@sentry/browser";
import { commonBeforeSend } from '@packages/sentry-config';

Sentry.init({
  // 各アプリケーション側の実装が冗長になり、アプリケーションに責任が寄ってしまう
  beforeSend: (event, hint) => {
    const e = commonBeforeSend('example-app', event, hint);
    if (!e) {
      return null;
    };

    if (e.message === 'アプリケーション固有で無視すべきエラー') {
      return null;
    }

    return e;
  },
});

また、次のようにSentry.init()のオプションを丸ごと返す関数として@packages/sentry-configに切り出すと、共通パッケージ側に責任を寄せることができるのでこの方法も個人的には好みです。

しかし、共通パッケージ側でbeforeSend以外の責任も持つことになるため共通フィルターを実装するという目的に対しては責任が若干重く、共通パッケージのアップデートやSentryクライアントのアップデートの際の影響範囲が広くなります。

// /packages/sentry-config/config.ts
import type { BrowserOptions } from "@sentry/browser";

export const createSentryConfig = (
  appName: string,
  options: BrowserOptions,
): BrowserOptions => ({
  ...options,
  beforeSend: (event, hint) => {
    if (isCommonIgnoreError(hint.originalException)) {
      return null;
    }

    event.tags['apps.name'] = appName;

    return options.beforeSend?.(event, hint) ?? event;
  },
});


// /apps/example-app/main.ts
import * as Sentry from "@sentry/browser";
import { createSentryConfig } from '@packages/sentry-config';

// アプリケーション側はスッキリかけますが、`createSentryConfig`の責任が重くなる
Sentry.init(createSentryConfig('example-app', {
  beforeSend: (event, hint) => {
    if (event.message === 'アプリケーション固有で無視すべきエラー') {
      return null;
    }
    return event;
  },
}));

そしてCustom Integrationを活用する場合は、切り出した実装をSentry.init()integrationsに渡すだけで良く、アプリケーション側のbeforeSendに影響を与えません。

共通フィルターの実装はIntegrationに隔離されるため、アプリケーション固有のフィルターを実装する場合は@packages/sentry-configの共通実装を意識せずにbeforeSendをシンプルに書くことができます。

@packages/sentry-configでアップデートがあった場合も共通フィルター以外に影響を与えないため、先述した方法と比べても責任が適切に分離されており、心理的安全性が高いです。

import type { Integration } from "@sentry/core";

// /packages/sentry-config/config.ts
export const myAppsIntegration = (appName: string): Integration => ({
  name: "MyAppsIntegration",
  processEvent(event, hint) {
    if (isCommonIgnoreError(hint.originalException)) {
      return null;
    }

    event.tags['apps.name'] = appName;

    return event;
  },
});


// /apps/web-app-1/main.ts
import * as Sentry from "@sentry/browser";
import { myAppsIntegration } from '@packages/sentry-config';

Sentry.init({
  // アプリケーション側の実装もシンプルで、共通フィルターだけで機能を分離できる
  integrations: [
    myAppsIntegration('example-app'),
  ],
  beforeSend: (event, hint) => {
    if (event.message === 'アプリケーション固有で無視すべきエラー') {
      return null;
    }
    return event;
  },
});

GMFではこのCustom Integrationの仕組みを利用して、一部のエラーを除外したりアプリケーションのメタ情報などをタグにセットして、日々のエラーの集計作業の改善を試みています。

さいごに

SentryのCustom IntegrationとbeforeSendの併用によって、共通フィルターとアプリケーション固有のフィルターをクリーンに分離するアプローチを紹介しました。Sentryで複数アプリケーションの共通フィルターを実装する際には是非Custom Integrationの採用を検討してみてください。

GameWithではエンジニアを絶賛募集中です。ご興味ありましたら是非カジュアル面談をお申し込みください!

github.com