関係ないJavaScriptのエラーをSentryに送信させない

ちきさんです。

Sentryでエラー収集してますか? してますよね。

さて、とあるWebアプリからSentryにエラーを送りたいが、下記の要件を満たす必要があったという話です。

(Sentry JavaScript SDK => Raven)

  1. Ravenをバンドルしてはいけない。(極限まで容量を削減する必要があるため)
  2. Sentryへのエラー送信がページ表示のパフォーマンスに影響を与えてはいけない。
  3. 同じページ上で発生する関係ないJavaScriptのエラーをSentryに送信してはいけない。
  4. 同じページに別のアプリのRavenが存在していたらそれを邪魔してはいけない。

なかなか厳しい要件ですね。

これらを満たすためにやったことは、

  • Ravenをバンドルせずにエラーを送信する必要が生じてから非同期でCDNから読み込む。
  • 同じページに別のアプリのRavenが既に存在していたらCDNから読み込まない。(エラー収集を諦める)
  • window.Ravenを変数に格納し直してそれを使う。
  • RavenがSentryにエラーを送信するためにwindow.onerrorを書き換えるので、エラー送信後に都度それを元に戻す。(uninstall())
  • エラー送信の度にinstall()uninstall()をセットで行う。

以上です。
無理やり感があるので公式である程度柔軟にできるように対応していただきたいものです。

サンプルコードは↓

SentryService.ts
export class SentryService {
  private isRavenRequested = false
  private isRavenLoadedByThisApp = false
  private isRavenSetupByThisApp = false
  private myRaven: any = null;

  private loadRaven(): Promise<void> {
    if ((window as any).Raven && !this.myRaven) {
      return Promise.reject('Raven is already loaded by other apps.');
    } else if (this.myRaven) {
      return Promise.resolve();
    }
    return new Promise((resolve, reject) => {
      if (this.isRavenRequested) {
        resolve();
        return;
      }
      const ravenVersion = '3.22.1'
      const s = window.document.createElement('script')
      s.src = `https://cdn.ravenjs.com/${ravenVersion}/raven.min.js`
      s.async = true
      s.crossOrigin = 'anonymous'
      s.onload = () => {
        this.isRavenLoadedByThisApp = true
        if ((window as any).Raven && !(window as any).Raven.isSetup() && (window as any).Raven.VERSION === ravenVersion) {
          this.myRaven = (window as any).Raven; // window.Ravenを変数にコピーして、
          delete (window as any).Raven; // window.Ravenを削除する。
          this.isRavenSetupByThisApp = true;
        } else {
          reject('Raven is already setup by other apps.')
        }
        resolve();
      }
      s.onerror = reject
      window.document.body.appendChild(s)
      this.isRavenRequested = true
    })
  }

  captureException(e: Error): void {
    this.loadRaven()
      .then(() => {
        if (this.myRaven && this.isRavenLoadedByThisApp && this.isRavenSetupByThisApp) {
          console.log('captureException', e)
          try {
            this.myRaven
              .config('https://<key>@sentry.io/<project>')
              .clearContext()
              .setTagsContext({
                type: 'foo',
                account: '123',
              })
              .setExtraContext({
                log: ['do something', 'next something', 'finally something'],
                state: {
                  x: 'bar',
                  y: true
                }
              })
              .install() // window.onerrorハンドラを書き換える。
              .captureException(e) // Sentryにエラーを送る。
              .uninstall(); // window.onerrorハンドラを元に戻す。
            console.log('Sentryにエラーを送りました。')
          } catch (_) {
            this.myRaven.uninstall()
          }
        } else {
          console.log('SentrySDKがまだ読み込まれていません。')
        }
      })
      .catch(console.log)
  }
}
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.