AstroにSentryを導入してみた


このブログにSentryを導入した。そういえば忘れていた

公式SDKの @sentry/astro をベースに、Cloudflare Worker環境に合わせた設定をメモしておく

環境

  • astro: ^5.13.5
  • @sentry/astro: ^10.36.0
  • @sentry/cloudflare: ^10.36.0

クライアントとエッジ(api)の両方をカバーするように設定している

インストール

npm install @sentry/astro @sentry/cloudflare

設定

今回は astro.config.ts でのインテグレーション追加に加えて、クライアント側とサーバー(Worker)側で個別に初期化ファイルを用意する構成にした。

astro.config.ts

import sentry from "@sentry/astro";
 
export default defineConfig({
  // ...
  integrations: [
    // ...
    sentry({
      sourcemaps: {
        filesToDeleteAfterUpload: ["./dist/**/*.map"],
      },
    }),
  ],
});

クライアントサイド (sentry.client.config.ts)

クライアント側の初期化設定。astro.config.ts に書くこともできるが、deplicagted な warning が出たので sentry のコンフィグ側に書いた

>[@sentry/astro] You passed in additional options (project, org, authToken, telemetry) to the Sentry integration.
>This is deprecated and will stop working in a future version. Instead, configure the Sentry SDK in your `sentry.client.config.(js|ts)` or `sentry.server.config.(js|ts)` files.
 
import * as Sentry from "@sentry/astro";
 
Sentry.init({
  dsn: import.meta.env.PUBLIC_SENTRY_DSN,
  tracesSampleRate: 1.0,
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,
  integrations: [
    Sentry.browserTracingIntegration(),
    Sentry.replayIntegration(),
  ]
});
 
// 匿名IDでのユーザー紐付け
if (typeof window !== "undefined") {
  // localStorageにUUIDを保存して setUser する
}

サーバーサイド (functions/_middleware.js)

Cloudflare Worker上で動かしているので、ミドルウェアで @sentry/cloudflare のプラグインを通す。これでWorker側のエラーも補足できるようになる

import * as Sentry from "@sentry/cloudflare";
 
export const onRequest = [
  Sentry.sentryPagesPlugin((context) => ({
    dsn: context.env.PUBLIC_SENTRY_DSN,
    tracesSampleRate: 1.0,
  })),
  async (context) => {
    const clientId = context.request.headers.get('cf-connecting-ip') || 'anonymous';
    if (context.data?.sentry) {
      context.data.sentry.setUser({ id: clientId, ip_address: clientId });
    }
    return context.next();
  },
];

環境変数の設定・取得

Sentryを動かすために必要な環境変数は以下。これらを .env やデプロイ環境のシークレットに設定しておく。

変数名用途参照タイミング
PUBLIC_SENTRY_DSNエラーの送信先URLビルド時・ランタイム(Client/Worker)
SENTRY_AUTH_TOKENソースマップのアップロード用トークンビルド時
SENTRY_ORGSentryの組織スラッグビルド時
SENTRY_PROJECTSentryのプロジェクト名ビルド時

各々取得方法は以下。sentryの画面から適宜作成・取得する

  • SENTRY_ORG / SENTRY_PROJECT
  • PUBLIC_SENTRY_DSN (Client Key) Sentryにログインし、対象の Project を選択 左メニューの [Settings] をクリック。Settingsメニュー下部にある[SDK Setup] セクションの [Client Keys (DSN)] を選択。DSNの項目、https://… で始まるURLをコピーする
  • SENTRY_AUTH_TOKEN (Auth Token) Sentryの組織設定、またはユーザー設定 [Settings] > [Organization] > [Developer Settings] > [Internal Integrations] の下部にある[New Token]から、新しいトークンを作成する こちらはソースマップのアップロードに用いる。必要な権限は Release:read&write Project:read&write Organization: read

デプロイ (GitHub Actions)

GitHub Actionsでビルド・デプロイする場合、ビルドステップでこれらの変数を渡す必要がある

      - name: Build Astro project
        run: npm run build
        env:
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          PUBLIC_SENTRY_DSN: ${{ secrets.PUBLIC_SENTRY_DSN }}

また、Cloudflare Worker側でも dsn を参照するため、wrangler-actionsecretsenv にも PUBLIC_SENTRY_DSN を含めておくとよい

ポイント

導入時の工夫

とりあえず導入するだけだとノイズも多いので、beforeSend で不要なエラーを間引くようにできる

...
  integrations: [
    Sentry.browserTracingIntegration(),
    Sentry.replayIntegration(),
  ],
...
  // 特定のエラーを通知しない
  beforeSend(event, hint) {
    const error = hint.originalException;
    if (
      error instanceof Error &&
      error.message.includes("Invalid origin")
    ) {
      return null;
    }
    return event;
  },
...

また、Cloudflare Worker の Middleware を使うことで、Astro インテグレーションから漏れがちなエッジ側のエラーも拾えるようにしたのが今回のこだわりポイント。

IP送信の制限

IPアドレスは立派な個人情報(PII)にあたる。これを収集してしまうとプライバシーポリシーの記述が複雑になったりする恐れがあるし、利用しないデータなので取得は避けた方が無難なため無効にしておく。

Sentryの設定画面(Settings -> Organization -> Security & Privacy)から Prevent Storing of IP Addresses をオンにすることで、IPアドレスを保持せずに運用できる。

IPを保存しない代わりに、Sentry.setUser で発行した匿名ID(Trace ID / Anonymous ID)を紐付けることで、特定のユーザーでエラーが頻発しているか、といった情報も追えるようにしている

コードはこんな感じ

// localStorageを使った匿名IDでのユーザー特定
if (typeof window !== "undefined") {
  const getOrCreateAnonymousId = () => {
    const STORAGE_KEY = "sentry_user_id";
    let id = localStorage.getItem(STORAGE_KEY);
    if (!id && typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
      id = crypto.randomUUID();
      localStorage.setItem(STORAGE_KEY, id);
    }
    return id;
  };
 
  const anonId = getOrCreateAnonymousId();
  if (anonId) {
    Sentry.setUser({ id: anonId });
  }
}

ソースマップの管理

astro.config.tsセクションの通り、ビルド後に .map ファイルが残らないよう、filesToDeleteAfterUpload で自動削除されるように設定した。

感想

SDKやドキュメントが充実していたおかげで思っていたよりはスムーズに導入できた

参考