Sentry SDKの設定にテストを書く
社内でSentry SDKを使っているプロダクトが複数あり、ここに設定していくのが大変だったので設定を共有化した。 (特にフロントエンドは共通で弾きたいエラーが多いので)
その際「設定が正しく書かれているか?」をテストしたくなり色々やってみた。
Sentry SDK設定の共通化方法
こんな感じのpackageを社内のgithub packagesで公開している。
import * as Integrations from "@sentry/integrations"; import * as Sentry from "@sentry/browser"; import { Event, EventHint } from "@sentry/types/dist/event"; const setDenyUrls = ( denyUrls: Sentry.BrowserOptions["denyUrls"] ): Sentry.BrowserOptions["denyUrls"] => { return [ // localhostでのエラーはsentryへ送らない(port numberは可変なため指定しない) /^http:\/\/localhost:\d+\//, // ページを保存した状態はsentryへ送らない /^file:/, // ngrokは送らない /^https?:\/\/\w+\.ngrok\.io\//, //拡張系のエラーは無視する /^(chrome|safari)-extension:\/\//, ...BaseDenyUrls ]; }; const setIntegrations = ( integrations: Sentry.BrowserOptions["integrations"] ): Sentry.BrowserOptions["integrations"] => { return ( integrations || [ new Integrations.CaptureConsole(), new Integrations.Dedupe(), new Integrations.ExtraErrorData(), ] ); }; const setIgnoreErrors = ( ignoreErrors: Sentry.BrowserOptions["ignoreErrors"] ): Sentry.BrowserOptions["ignoreErrors"] => { return [ ...(ignoreErrors || []), "onSvFinishLoading", "Failed to fetch", "Network request failed", "timeout of 0ms exceeded", "サーバに接続できませんでした", "ネットワーク接続が切れました", "キャンセルしました", "Đã mất kết nối mạng.", "AbortError: Aborted", "Network Error", "Request aborted", "A network error occurred", "window.webkit.messageHandlers", ]; }; type MatcherType<l extends string r> = { [key in L]: () => R }; const extraFilter = <r>( extra: Event["extra"], matcher: MatcherType ): R => { if ("string" !== typeof extra?.["xxx"]) { return matcher.unmatch(); } if (extra["xxx"].match(/xxx/)) { return matcher.match(); } return matcher.unmatch(); }; const contextsFilter = <r>( contexts: Event["contexts"], matcher: MatcherType ): R => { if ("object" !== typeof contexts?.["ServerParseError"]) { return matcher.unmatch(); } if ("string" !== typeof contexts["ServerParseError"]["bodyText"]) { return matcher.unmatch(); } const bodyText = contexts["ServerParseError"]["bodyText"]; if (bodyText.match(/xxx/i)) { return matcher.match(); } return matcher.unmatch(); }; export function makeInit (options: Sentry.BrowserOptions): Sentry.BrowserOptions { if ("enabled" in options && options.enabled === false) { return options; } options.denyUrls = setDenyUrls(options.denyUrls); options.integrations = setIntegrations(options.integrations); options.ignoreErrors = setIgnoreErrors(options.ignoreErrors); const beforeSend = options.beforeSend; options.beforeSend = (event, hint?: EventHint) => { return extraFilter(event.extra, { match: () => null, unmatch: () => contextsFilter(event.contexts, { match: () => null, unmatch: () => beforeSend?.(event, hint) || event, }), }); }; return options; }
Sentry SDK設定に対するテスト
こんな感じでテストを書いている。 (DevDependenciesに@sentry/nodeを含めている)
以下の方法はsentry@v7で動かなくなった。
Sentry Testkit for JavaScript | Sentry Documentationが案内されていたので、- Sentry-Testkit - Testing Sentry Reports Became Easyを使用する。
import * as Sentry from "@sentry/node"; import { Transport } from "@sentry/types/dist/transport"; import { Status } from "@sentry/types/dist/status"; import SentrySetting from "../src"; import { NodeOptions } from "@sentry/node/dist/types"; import { captureEvent } from "@sentry/minimal"; class TestTransport implements Transport { public static sendEventMock = jest.fn(); sendEvent(event) { TestTransport.sendEventMock(event); return Promise.resolve({ status: Status.Skipped, }); } close() { return Promise.resolve(true); } } const ExpectMockCall = async (result: "filtered" | "throwing") => { // fakeTimerもsetTimeout(r)もだめだったので50ms待つ // (実際は10msでも行けたけど念の為5倍) await new Promise<void>((r) => setTimeout(r, 50)); const count = TestTransport.sendEventMock.mock.calls.length; expect(count ? "throwing" : "filtered").toStrictEqual(result); }; describe("Sentry SDK settings", () => { afterEach(() => { TestTransport.sendEventMock = jest.fn(); }); const sentryOptions: NodeOptions = { dsn: "正しいDNSをSentry設定画面から取得して書く。ただし、Transportで送信をブロックしているためSentryへは送信されない", transport: TestTransport, }; it("mock call", async () => { Sentry.init(sentryOptions); Sentry.captureException(new Error("test")); await ExpectMockCall("throwing"); }); it.each([ ["filtered", "onSvFinishLoading"], ["filtered", "Request aborted"], // ignoreErrorsは部分一致なので、追加の文字があってもフィルタされる ["filtered", "Request aborted hoge"], ["throwing", "hoge"], ] as const)("ignoreErrors: %s %s", async (result, error) => { Sentry.init(SentrySetting.makeInit(sentryOptions)); Sentry.captureException(new Error(error)); await ExpectMockCall(result); }); it.each([ // このファイルのファイル名を書く ["filtered", "sentry.test.ts"], ["throwing", "hoge"], ] as const)("denyUrls: %s %s", async (result, url) => { Sentry.init( SentrySetting.makeInit({ ...sentryOptions, denyUrls: [url], }) ); Sentry.captureException(new Error("error")); await ExpectMockCall(result); }); it.each([ ["filtered", "xxx"], ["throwing", "{}"], ] as const)("extraFilter: %s %s", async (result, xxx) => { Sentry.init(SentrySetting.makeInit(sentryOptions)); Sentry.captureEvent({ extra: { xxx, }, }); await ExpectMockCall(result); }); it.each([ ["filtered", "xxx"], ["throwing", "{}"], ] as const)("contextsFilter: %s %s", async (result, bodyText) => { Sentry.init(SentrySetting.makeInit(sentryOptions)); Sentry.captureEvent({ contexts: { ServerParseError: { bodyText, }, }, }); await ExpectMockCall(result); }); });
これで「実際にエラーが起こった場合に正しくFilterされるか?」をテストできる。
ちなみに、上記で設定している extra はSentryの設定画面で言うところの ADDITIONAL DATA にあたる。
テスト書いてて気づいたけど、 ignoreErrors は文字列指定では部分一致なので注意。 (完全一致にする場合、正規表現で ^xxx$ と指定する必要がある)













