コンテンツにスキップ

Vendure 監視・運用設計書

概要

Fly.io + 専用 Redis アプリで運用される Vendure システムの監視・運用戦略を説明します。

[!IMPORTANT] 継続監視の正式導線は Sentry Uptime / Uptime Kuma / GitHub Actions fallback とする。observability/ 配下の Grafana / Prometheus / Loki / log shipper は、現時点では Fly.io の正式運用 app ではなく、ローカル確認または将来導入用テンプレートとして扱う。

現在の verification / monitoring lanes

[!TIP] requestId / traceId / spanId / workflowTraceId の役割分離と、Playwright manifest / workflow archive / Sentry diagnostics の相関方法は Observability 相関ガイド を正本とする。

subtle-error detection layers(現在の正規導線)

Layer Primary automation Manual entrypoint 役割
browser synthetic scheduled-dashboard-smoke.yml / dashboard post-deploy smoke just dashboard-smoke-postdeploy production / just dashboard-smoke-postdeploy staging React Dashboard の login / runtime / error boundary の false green を、/products /product-variants /customers /orders の list に加え /products/:id /customers/:id /orders/:id detail 画面まで含めて継続検知する。
dashboard admin API smoke _deploy-dashboard-workers.yml / _deploy-vendure-fly.yml / scheduled-dashboard-smoke.yml just dashboard-admin-api-smoke production / just dashboard-admin-api-smoke staging React Dashboard が依存する Admin API の auth / list / asset upload を browser synthetic とは別レーンで fail-fast する。products 系に加えて customers / orders も確認する。
readiness contract check-ready-schema-drift.sh / verify-vendure-deploy / Sentry Uptime / Uptime Kuma just monitor-endpoints-check Storefront / Vendure readiness・schema drift を deploy 前後と継続監視の両方で止める。
WordPress drift audit wordpress-drift-audit.yml just wp-drift-audit / just wp-drift-audit-vps staging / just wp-drift-audit-vps production WordPress CMS の plugin / ACF JSON / option / fixed page 構成 drift を read-only で検知し、DB テーブル差分と混同せずに切り分ける。
Sentry browser/runtime alerts observability/sentry-workflow-alerts.json + scripts/ops/sentry-workflow-alerts.mjs just sentry-alerts-list / just sentry-alerts-upsert-dry-run / just sentry-alerts-upsert browser/runtime の first-seen・regression・dependency spike を notification layer として拾う。
monitor / alert drift audit scripts/ops/sentry-live-config-audit.mjs just sentry-live-config-audit -- --allow-drift --json checked-in の Sentry workflow / uptime monitor 設定と live state の drift を即座に見つける。
deploy gate _deploy-dashboard-workers.yml / _deploy-vendure-fly.yml / deploy-prod-preview.yml / prod-preview-smoke-storefront.yml 上記 manual entrypoint の再実行 + workflow rerun release 前後に「緑だが壊れている」状態を残さない。prod-preview は未リリース Storefront / Dashboard を production data plane へ接続する限定公開 gate として扱う。追加の Vendure Fly app / machine / DB は作らず、Cloudflare Access 配下で短時間だけ確認する。
business-canary / KPI proxy lane staging-smoke-storefront.yml / prod-preview-smoke-storefront.yml / production-smoke-storefront.yml just storefront-business-canary staging / just storefront-business-canary prod-preview / just storefront-business-canary production / just storefront-shadow-probe production login / browse / cart の主要導線を KPI proxy として継続確認する。staging / prod-preview / production ともに shadow-readonly probe を先に流し、遅延や data anomaly も同時に拾う。staging のみ代引 / 振込 / 再注文の注文成立まで担保する。prod-preview は production-smoke channel/principal を使い、default channel に注文を作らない。business-canary 失敗時は deploy/Access 問題と production-smoke fixture 運用問題を切り分ける。
  • Continuous-safe (Sentry): observability/external-monitor-endpoints.json + scripts/ops/sentry-uptime-monitors.mjs
  • Trigger: 5 分ごと
  • Isolation: Sentry Uptime
  • 役割: production の Storefront readiness / Vendure readiness / Vendure schema drift を継続監視
  • Continuous-safe (Uptime Kuma): observability/external-monitor-endpoints.json + scripts/ops/uptime-kuma-monitors.mjs
  • Trigger: 5 分ごと
  • Isolation: Uptime Kuma
  • 役割: production の Storefront readiness / Vendure readiness / Vendure schema drift / WordPress login を独立監視
  • Fallback safe probes: scheduled-safe-probes.yml
  • Trigger: 10 分ごと / 手動
  • Isolation: safe probe
  • 役割: staging / production の version / React Dashboard login / WordPress を含む fallback 確認。Vendure ready は schemaDrift == "ok" まで含めて判定する
  • WordPress drift audit: wordpress-drift-audit.yml
  • Trigger: apps/wordpress-cms/** または just/wordpress.just の PR / 毎週定期実行 / 手動
  • Isolation: local docker compose + read-only CMS audit
  • 役割: WordPress bootstrap 後の CMS 構成が、checked-in の plugin / ACF JSON / option 前提と一致するかを CI で固定監査する
  • 手動再現: just wp-drift-audit。live の切り分けは just wp-drift-audit-vps staging / production
  • WordPress live drift monitor: wordpress-drift-monitor.yml
  • Trigger: 毎週定期実行 / 手動
  • Isolation: authenticated WordPress REST endpoint (/wp-json/ritsubi/v1/drift-audit) を read-only 実行
  • 役割: staging / production の live WordPress が deploy 済み plugin と options 構成から drift していないかを、local compose とは独立に確認する
  • 前提: AWS Secrets Manager 上の WORDPRESS_ENDPOINTCMS_API_TOKEN が正しく同期されていること
  • Periodic React Dashboard smoke: scheduled-dashboard-smoke.yml
  • Trigger: 6 時間ごと / 手動
  • Isolation: browser synthetic
  • 役割: React Dashboard の runtime error / error boundary / admin API wiring の false green を継続検知
  • 手動再現: just dashboard-admin-api-smoke production / just dashboard-smoke-postdeploy production(staging も同様)
  • Admin API smoke script: apps/vendure-server/scripts/dashboard-api-canary.ts
  • Trigger: dashboard deploy 後 / Vendure deploy 後 / scheduled-dashboard-smoke.yml / 手動
  • Isolation: authenticated read-only GraphQL canary
  • 役割: React Dashboard が依存する Admin API contract(products, featuredAsset, productVariants, customers, orders)を browser 描画前に検知する
  • 判定: login 成功後は session cookie または vendure-auth-token header のどちらでも継続認証できれば成功とみなす
  • Business-canary / KPI proxy: production-smoke-storefront.yml / staging-smoke-storefront.yml
  • Trigger: deploy 後 / 手動
  • Isolation: shadow-readonly + critical flow synthetic
  • 役割: staging では shadow-readonly + critical auth + staging checkout、production では shadow-readonly + prod-safe synthetic を使い、login / browse / add-to-cart / checkout 表示までを business impact proxy として確認する
  • 注文成立 (staging 限定): deploy gate は外部 SBPS contract に依存しない checkout.staging-gate.real.spec.ts の銀行振込 checkout で注文完了を確認する。SBPS live checkout は #556 の vendor-side 再確認として手動 opt-in のみ許容し、checkout.sbps.real.spec.ts の staging/local guard により production URL へ向いた実決済は fail-closed で拒否する。
  • 手動再現: just storefront-shadow-probe production / just storefront-business-canary production(staging も同様)。注文成立フローの staging ローカル実行は just storefront-e2e-real staging "tests/e2e/scenarios/standard-purchase-flow.real.spec.ts tests/e2e/scenarios/bank-transfer-purchase-flow.real.spec.ts tests/e2e/scenarios/reorder-from-history-flow.real.spec.ts tests/e2e/scenarios/gift-code-and-points-purchase-flow.real.spec.ts"。SBPS real checkout は RUN_SBPS_E2E=true just storefront-e2e-real staging "tests/e2e/checkout.sbps.real.spec.ts --project=chromium" を使う。決済方法別の注文成立を smoke 層で一括確認したい場合は just storefront-payment-purchase-smoke staging(非リダイレクト 3 種+staging secret の RUN_SBPS_E2E で SBPS クレカ)を使う。
  • Pre-release hidden lane: deploy-prod-preview.yml + prod-preview-smoke-storefront.yml
  • Trigger: Staging Smoke Storefront 成功後 / 手動
  • Isolation: Cloudflare Access + production-smoke channel + shadow-readonly
  • 役割: 未リリース Storefront / React Dashboard build を production Vendure / CMS / assets / Secrets へ接続し、public production traffic へ出す前に確認する。 promote-main.yml の前段 gate
  • Post-deploy / periodic: production-smoke-storefront.yml
  • Trigger: Deploy Production 成功後 / 6 時間ごと / 手動
  • Isolation: synthetic + shadow-readonly
  • 役割: production の business-canary / KPI proxy lane を継続確認
  • Observability canary: production-sentry-smoke-storefront.yml
  • Trigger: 手動
  • Isolation: safe probe + Sentry synthetic
  • 役割: production observability 導線の個別切り分け

Sentry Web Vitals monitoring

Storefront と React Dashboard は、Sentry の browser tracing に加えて Core Web Vitals 相当値を Sentry metrics に送信する。transaction は個別 pageload / navigation の深掘り、Web Vitals metrics はページ表示速度の時系列・閾値監視に使う。

Surface Metric name pattern Attributes Unit
Storefront storefront.web_vitals.{ttfb,fcp,lcp,cls,inp} surface, page millisecond / none
React Dashboard react_dashboard.web_vitals.{ttfb,fcp,lcp,cls,inp} surface, page millisecond / none
  • ttfb: navigation timing の responseStart
  • fcp: first-contentful-paint
  • lcp: ページ非表示化または pagehide 時点の latest largest contentful paint
  • cls: recent input を伴わない layout shift の累積値
  • inp: interaction entry の最大 duration

初期の目安は次を使う。production の実測が溜まったら p75 / p95 を見て alert threshold を更新する。

  • lcp: 2500ms 以下を目安にし、同じ page の pageload transaction、画像配信、 backend fetch span、Cloudflare cache 状態を見る。
  • inp: 200ms 以下を目安にし、Sentry replay と長い task / interaction 前後の browser issue を確認する。
  • cls: 0.1 以下を目安にし、対象 page の late-loaded image / banner / header / font の layout shift を確認する。
  • ttfb: 800ms 以下を目安にし、Storefront Worker / Vendure / WordPress の upstream span と Sentry Uptime の直近失敗を確認する。
  • fcp: 1800ms 以下を目安にし、初期 bundle、font、critical CSS、Cloudflare asset delivery を確認する。

Sentry UI では、まず Metrics / Dashboards で surfacepage を絞り込む。regression が見えた場合は同じ時間帯の Performance transaction、 Replay、browser/runtime issue を同じ page と release で確認する。

PerformanceObserver 非対応 browser では metric は送信されない。これは速度監視の補助観測面であり、 Sentry SDK の error / tracing 初期化や通常の browser runtime alert を置き換えない。

[!IMPORTANT] production verification は production-smoke channel + shadow-readonly を正式運用とする。default channel では read-only / cart cleanup までに留め、 注文作成を伴う synthetic は専用 production-smoke channel、no-op payment、 smoke marker を前提にする。

periodic な readiness 外形監視は Sentry Uptime + Uptime Kuma を正規運用とし、 .github/workflows/scheduled-safe-probes.ymlmanual fallback とする。

production verification の運用ルール

  1. synthetic principal
  2. production browser smoke は default channel 上の prod-safe smoke と read-only probe に限定する。
  3. 資格情報の正本は AWS Secrets Manager prod/storefront 配下の E2E_LOGIN_EMAIL / E2E_LOGIN_PASSWORD とする。
  4. fallback credential は使用しない。
  5. synthetic principal は、対象導線で使う商品 / 価格 / visibility policy に適合していること。特に商品詳細 smoke は「ログインできる」だけでは不十分で、 validateVisibility を通る顧客である必要がある。
  6. カート状態は実行前後に cleanup し、shippingMode を含めて初期化する。
  7. shadow-readonly lane
  8. read-only shadow probe は /api/health/live /api/health/ready /api/version /auth/login に加え、/commerce/shop-api への read-only query だけを実行する。
  9. GraphQL は search / products / collections / product(slug) の read-only query を確認し、latency budget と minimum data threshold の両方で判定する。
  10. mutation、注文確定、決済確定、メール送信を伴う確認は production の shadow lane に含めない。
  11. default channel に対する mutation も production smoke には含めない。
  12. failure triage
  13. deploy-prod-preview.yml / prod-preview-smoke-storefront.yml / production-smoke-storefront.yml が失敗したら、まず workflow log archive と Playwright artifact を確認する。
  14. artifact を開くときは、workflow archive / job attachment / Sentry diagnostics / Playwright manifest を同じ workflowTraceId / traceId で辿る。attachment artifact の manifest.json にある detectedManifests[] を起点に、どの nested manifest を先に見るべきか判断する。
  15. shadow probe が先に失敗している場合は、Storefront /api/version とVendure /version の build metadata、ならびに Sentry Uptime monitor Production Storefront readiness / Production Vendure readiness / Production Vendure schema drift の直近失敗有無を確認する。必要なら scheduled-safe-probes.yml を manual fallback として再実行する。
  16. synthetic smoke のみ失敗している場合は、synthetic 顧客の cart 状態、login 可否、対象商品の可視性変更有無を確認する。
  17. WordPress リッチ説明だけが欠ける場合は、まず direct WP GraphQL で wordPressProductDetailproductDetailMeta.styleMode を確認する。 styleMode が direct WP に存在しない場合は production WordPress が古い schema のままなので、just update-wp-production を優先する。
  18. direct WP では新しい記事が見えるのに Vendure shop-api で古い null / default が返る場合は、Vendure の WordPress client cache(最大 5 分)を疑う。 revalidate/cms は Storefront cache だけを消す点に注意する。
  19. observability だけが失敗している場合は production-sentry-smoke-storefront.yml と Sentry 側の Feedback / release 状態を確認する。

dashboardApiCanary 調査の実運用メモ

/health/readydashboardApiCanary は、2026-04 以降は startup 一回だけの値ではなく 定期 refresh されます。したがって triage では次を見ます。

  1. checkedAt
  2. 古い: 古い runtime が残っているか、deploy が切り替わっていない
  3. 新しい: 現在の runtime で本当に fail している
  4. error
  5. login succeeded but no session cookie was returned
    • localhost probe では cookie 非返却でも vendure-auth-token header が返る場合がある
    • manual just dashboard-admin-api-smoke production が pass しているなら、 localhost canary の偽陰性を疑う
  6. manual canary
just dashboard-admin-api-smoke production
curl -sS https://commerce.ritsubi-platform.com/health/ready | jq '{status, dashboardApiCanary, schemaDrift}'
  1. deploy 後チェック
  2. dashboardApiCanary=ok
  3. schemaDrift=ok
  4. app machine checks passing

Fly release bookkeeping が監視を汚すとき

本番では live machine が healthy でも、Fly の release history だけが失敗扱いで残ることがあります。監視の正本は release history 単独ではなく 次の組み合わせです。

  • flyctl machine list -a ritsubi-ecommerce
  • flyctl checks list -a ritsubi-ecommerce --json
  • curl -sS https://commerce.ritsubi-platform.com/health/ready | jq

ghost machine が残っている場合は、release history を見続ける前にまず machine inventory を整理してください。

監視アーキテクチャ

監視対象とメトリクス

graph TB
    subgraph "Application Layer"
        V[Vendure API]
        NS["Storefront (Vite)"]
        W[Worker Process]
        WP[WordPress CMS]
    end

    subgraph "Infrastructure Layer"
        PG[(PostgreSQL)]
        R[(Redis)]
        FS[File Storage]
    end

    subgraph "Current Monitoring"
        SU[Sentry Uptime]
        UK[Uptime Kuma]
        GH[GitHub Actions Safe Probes]
        SN[Sentry Issues / Logs / Replay]
        STD[Platform stdout logs]
    end

    V --> PG
    V --> R
    V --> FS
    NS --> V
    W --> V

    V --> SN
    NS --> SN
    W --> SN

    V --> STD
    NS --> STD
    W --> STD

    SU --> V
    SU --> NS
    UK --> V
    UK --> NS
    UK --> WP
    GH --> V
    GH --> NS
    GH --> WP

1. アプリケーション監視

1.1 パフォーマンスメトリクス

レスポンス時間監視

// performance-metrics.ts
import { performance } from "perf_hooks";
import { Logger } from "@vendure/core";

export class PerformanceMonitor {
  private logger = new Logger(PerformanceMonitor.name);

  trackGraphQLQuery(operationName: string, duration: number) {
    this.logger.verbose(`GraphQL ${operationName}: ${duration}ms`);

    // Prometheus メトリクス
    graphqlDurationHistogram.labels({ operation: operationName }).observe(duration / 1000);

    // SLA閾値チェック
    if (duration > 2000) {
      this.logger.warn(`Slow query detected: ${operationName} took ${duration}ms`);
    }
  }

  trackAPIEndpoint(endpoint: string, method: string, statusCode: number, duration: number) {
    apiDurationHistogram
      .labels({ endpoint, method, status: statusCode.toString() })
      .observe(duration / 1000);

    apiRequestsTotal.labels({ endpoint, method, status: statusCode.toString() }).inc();
  }
}

// Prometheus メトリクス定義
import { register, Histogram, Counter } from "prom-client";

export const graphqlDurationHistogram = new Histogram({
  name: "vendure_graphql_duration_seconds",
  help: "GraphQL query duration",
  labelNames: ["operation"],
  buckets: [0.1, 0.5, 1, 2, 5, 10],
});

export const apiDurationHistogram = new Histogram({
  name: "vendure_api_duration_seconds",
  help: "API endpoint duration",
  labelNames: ["endpoint", "method", "status"],
  buckets: [0.1, 0.5, 1, 2, 5, 10],
});

export const apiRequestsTotal = new Counter({
  name: "vendure_api_requests_total",
  help: "Total API requests",
  labelNames: ["endpoint", "method", "status"],
});

register.registerMetric(graphqlDurationHistogram);
register.registerMetric(apiDurationHistogram);
register.registerMetric(apiRequestsTotal);

ビジネスメトリクス

// business-metrics.ts
export class BusinessMetrics {
  private logger = new Logger(BusinessMetrics.name);

  // 注文関連メトリクス
  trackOrder(order: Order) {
    orderTotal.labels({ status: order.state }).observe(order.total);

    ordersCreatedTotal
      .labels({
        customerType: this.getCustomerType(order.customer),
        channel: order.channels[0]?.code || "default",
      })
      .inc();
  }

  // 商品関連メトリクス
  trackProductView(productId: string, customerId?: string) {
    productViewsTotal.labels({ productId }).inc();

    if (customerId) {
      customerActivityTotal.labels({ customerId, action: "product_view" }).inc();
    }
  }

  // B2B特有メトリクス
  trackRebateCalculation(customerId: string, amount: number, period: string) {
    rebateAmountGauge.labels({ customerId, period }).set(amount);

    rebateCalculationsTotal.labels({ period }).inc();
  }

  // キャンペーン効果測定
  trackCampaignUsage(campaignId: string, discount: number) {
    campaignUsageTotal.labels({ campaignId }).inc();

    campaignDiscountTotal.labels({ campaignId }).observe(discount);
  }

  private getCustomerType(customer: Customer): string {
    // B2B顧客タイプの判定ロジック
    const customFields = customer.customFields as any;
    return customFields?.customerStatus || "general";
  }
}

// ビジネスメトリクス定義
export const orderTotal = new Histogram({
  name: "vendure_order_total_amount",
  help: "Order total amount",
  labelNames: ["status"],
  buckets: [1000, 5000, 10000, 50000, 100000, 500000],
});

export const ordersCreatedTotal = new Counter({
  name: "vendure_orders_created_total",
  help: "Total orders created",
  labelNames: ["customerType", "channel"],
});

export const productViewsTotal = new Counter({
  name: "vendure_product_views_total",
  help: "Total product views",
  labelNames: ["productId"],
});

export const rebateAmountGauge = new Gauge({
  name: "vendure_rebate_amount",
  help: "Customer rebate amount",
  labelNames: ["customerId", "period"],
});

1.2 エラー監視

Sentry 統合

現在の対象 surface と project 対応

Surface 主な実行面 Project slug の解決順 release 名 現在の状態
Storefront Vite browser bundle + Worker API on Cloudflare Workers SENTRY_PROJECT_STOREFRONTSENTRY_PROJECT → 既定値 b2b-commerce-storefront storefront@<git-sha> release 作成、commit association、deploy marker、sourcemap upload、runtime enrichment、browser SDK 初期化、Session Replay、主要 API / client context の failure capture、warn/error の Sentry Logs、browser failed request capture が有効。Sentry Feedback widget は関係者向け導線として ?sentry-feedback=1 指定時のみ表示する。Worker runtime traces / logs は Cloudflare Observability OTLP export を正本とし、Sentry へ配送する。
Vendure NestJS / GraphQL API on Fly.io SENTRY_PROJECT_VENDURESENTRY_PROJECT vendure@<git-sha> release 作成、commit association、deploy marker、sourcemap upload、runtime enrichment、Node Profiling、SMILE 定期 task と worker heartbeat monitor が有効。staging / production ともに CI build artifact から sourcemap を upload し、Fly runtime の --enable-source-maps は fallback として維持する
React Dashboard browser bundle on Cloudflare Workers SENTRY_PROJECT_DASHBOARDSENTRY_PROJECT dashboard@<git-sha> release 作成、commit association、deploy marker、sourcemap upload、browser SDK 初期化、Session Replay、warn/error の Sentry Logs と browser failed request capture が有効。Sentry Feedback widget は搭載しない(2026-05 削除)。admin API request は x-request-id / request_idactor_id / actor_type / dashboard_admin context で Vendure backend event と相関できる
WordPress Plugin PHP runtime / browser in custom plugin on VPS (Webarena Indigo) WORDPRESS_PLUGIN_SENTRY_RELEASESENTRY_RELEASE(release), browser DSN: WORDPRESS_BROWSER_SENTRY_DSNSENTRY_DSN ritsubi-ec-plugin@<git-sha> 自作 plugin を独立 surface として扱い、plugin header version を tag/context の正本にする。PHP runtime の fatal error / uncaught exception と browser の JS error は plugin metadata を付けて capture し、release 未注入時は ritsubi-ec-plugin@<plugin-version> を既定値にする。just wp-deploy-vps は deploy ごとに plugin release/environment を自動注入し、必要な Sentry credentials があれば deploy marker を作成する。package workflow も同じ release contract で zip artifact を生成する。

[!IMPORTANT] この runbook は「現在の実装」を正本とする。Vendure は staging / production ともに release / commit association / deploy marker に加えて CI から sourcemap upload を行う。Fly runtime の --enable-source-maps は fallback として残すが、triage の正本は upload 済み artifact とする。

現時点の Sentry フル活用は release / sourcemap / diagnostics / feedback / Storefront / React Dashboard Session Replay / Logs / browser failed request capture / workflow alerts / uptime monitors / recurring task crons / Vendure profiling / worker heartbeat monitors / Storefront Worker traces+logs の Cloudflare Observability OTLP export を正式運用とし、AI monitoring は対象外 とする。

必須 env / secrets

Runtime 共通(Storefront server/edge・Vendure・WordPress PHP)

  • SENTRY_DSN: DSN。未設定ならその runtime の Sentry 送信は無効。
  • SENTRY_ENVIRONMENT: 環境名。未設定時は NODE_ENV を利用。
  • SENTRY_RELEASE: runtime が付与する release 名。CI deploy で surface ごとに注入する。
  • SENTRY_TRACES_SAMPLE_RATE: tracing のサンプリング率。
  • SENTRY_SEND_DEFAULT_PII: true の場合のみ標準 PII を送信。

WordPress 追加メモ

  • WordPress は SDK 依存ではなく、apps/wordpress-cms/plugins/ritsubi-ec-plugin から DSN endpoint へ直接 event を送る。
  • browser runtime は browser.sentry-cdn.com の bundle を読み込み、plugin 側フックから WordPress login / admin で JS error を送る。
  • 現在の公開面の大半は Storefront へ redirect されるため、WordPress browser surface は実運用上 login / admin が中心になる。
  • browser 専用 env (WORDPRESS_BROWSER_SENTRY_*) が無い場合は backend と同じ SENTRY_* を流用する。
  • SENTRY_TRACES_SAMPLE_RATE は WordPress では現時点未使用。

Storefront browser 追加

  • VITE_PUBLIC_SENTRY_DSN
  • VITE_PUBLIC_SENTRY_ENVIRONMENT
  • VITE_PUBLIC_SENTRY_RELEASE
  • VITE_PUBLIC_SENTRY_TRACES_SAMPLE_RATE
  • VITE_PUBLIC_SENTRY_SEND_DEFAULT_PII
  • VITE_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
  • VITE_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
  • SENTRY_DEBUG_TOKEN(任意): staging / development の疎通確認用。/api/debug-sentry/api/client-logs の debug trigger で使用し、production では受け付けない。/api/client-logs は non-production でも same-origin request を必須とする。
  • SENTRY_DEBUG_SIGNING_SECRET(staging canary 用): staging-sentry-smoke-storefront.yml が run marker 束縛の request token を導出するための専用 secret。SENTRY_DEBUG_TOKENCMS_REVALIDATE_SECRET のような別責務の値を流用しない。

React Dashboard build-time 追加

  • VITE_ADMIN_API_URL
  • VITE_SENTRY_DSN
  • VITE_SENTRY_ENVIRONMENT
  • VITE_SENTRY_RELEASE
  • VITE_SENTRY_TRACES_SAMPLE_RATE
  • VITE_SENTRY_SEND_DEFAULT_PII
  • Replay sample rate はコード定数(production: session 10%、non-production: session 100%、on-error 100%)を使い、Secrets Manager では管理しない。

WordPress browser 追加

  • WORDPRESS_BROWSER_SENTRY_DSN
  • WORDPRESS_BROWSER_SENTRY_ENVIRONMENT
  • WORDPRESS_BROWSER_SENTRY_RELEASE
  • WORDPRESS_BROWSER_SENTRY_SEND_DEFAULT_PII

[!NOTE] Sentry Feedback widget は Storefront のみで利用する(?sentry-feedback=1 指定時のみ表示)。既存の browser DSN を再利用するため、Userback token のような追加 secret は不要。React Dashboard では feedback widget を搭載していない。

CI / release automation

  • SENTRY_AUTH_TOKEN: AWS Secrets Manager b2b-ecommerce/ci/shared から取得する。release/sourcemap に加え、workflow alert upsert と uptime monitor upsert でも利用するため、対象 project の monitor 作成 / 更新権限を持つ token を使う。
  • local の just sentry-uptime-list / just sentry-uptime-upsert / just sentry-alerts-list / just sentry-alerts-upsert / just sentry-issues は、既定で login 済みの sentry user auth を使う。
  • just sentry-uptime-list-cli は raw な sentry api organizations/ritsubi/uptime/ を直接叩く確認用 escape hatch とする。
  • SENTRY_AUTH_TOKEN を明示 inject した場合のみ、repo script は CI と同じ token transport を使う。したがって local で token は不要、CI / release automation では引き続き必要、という役割分担にする。
  • SENTRY_ORG: 未設定時は root .sentryclircritsubi を利用。
  • SENTRY_PROJECT_STOREFRONT
  • SENTRY_PROJECT_VENDURE
  • SENTRY_PROJECT_DASHBOARD
  • SENTRY_REPOSITORY(任意): commit association 用 repository slug の上書き。
  • SENTRY_URL(任意): self-hosted Sentry 利用時のみ。
  • observability/sentry-workflow-alerts.json: checked-in の workflow alert 定義。既定の通知先は email issue owners(fallback: Active Members)。
  • observability/sentry-live-config-audit.json: Sentry live config audit の suppressions。現在は issue #533 に紐づく uptime drift をここで管理する。
  • observability/external-monitor-endpoints.json: checked-in の外形監視 endpoint catalog。Sentry / Uptime Kuma の共通正本。
  • scripts/ops/check-external-monitor-endpoints.mjs: shared endpoint catalog を使った手動確認 CLI。just monitor-endpoints-check から一発確認できる。
  • scripts/ops/sentry-uptime-monitors.mjs / scripts/ops/uptime-kuma-monitors.mjs: shared endpoint catalog から各 tool 向け monitor 定義を実行時に構築し、そのまま list / dry-run / upsert を行う。

release / sourcemap / commit association の流れ

  1. 各 deploy workflow が scripts/ops/sentry-release.mjs prepare を実行し、surface ごとの release を作成する。
  2. 同時に --sha--previous-sha で commit association を登録する。
  3. build 後、sourcemap 対応済み surface では debug-id 注入 → upload → .map 除去まで CI で完結させる。Vendure は production のみ apps/vendure-server/dist を CI 上で build して upload し、staging は build 時に生成した TypeScript source map を Fly runtime の --enable-source-maps で解決する。
  4. deploy 成功後に scripts/ops/sentry-release.mjs finalize を実行し、release URL を更新して released 状態にする。

マニュアルデプロイ後の Sentry エラー確認手順

CI を経由しないマニュアルデプロイ(緊急デプロイ・手動 rollback 等)では、Sentry release marker が自動作成されないため、以下の手順を踏む。

Step 1 — release marker を作成する
just sentry-mark-manual-deploy <surface> <sha> <env>
# 例: just sentry-mark-manual-deploy storefront abc1234 production
# 例: just sentry-mark-manual-deploy vendure abc1234 staging
# surface: storefront | vendure | dashboard

これは内部で sentry-release.mjs prepare → finalize → deploy を順に実行し、 Sentry 上に <surface>@<sha> という release とデプロイ履歴を登録する。

Step 2 — デプロイ起因エラーを確認する
just sentry-issues-release <surface> <sha>
# 例: just sentry-issues-release storefront abc1234
# JSON 出力: just sentry-issues-release storefront abc1234 json

release:<surface>@<sha> クエリで unresolved issues を絞り込む。

CI deploy の場合(marker 自動作成済み)

CI deploy は prepare-sentry-release / finalize-sentry-release / create-sentry-deploy action が marker を自動作成するため、Step 1 は不要。 デプロイ後は Step 2 のみでよい。

時刻ベースの代替確認方法(release marker が存在しない場合)
SENTRY_ISSUES_QUERY="is:unresolved firstSeen:>2026-05-09T10:00:00Z" \
SENTRY_ISSUES_PROJECT=storefront \
  just sentry-issues

sentry issue list を直接使う場合、時間範囲の flag は Sentry API の statsPeriod ではなく CLI の --period を使う(例: sentry issue list ritsubi/b2b-commerce-storefront --query 'is:unresolved' --period 14d)。

workflow alert automation(現在の正式運用)

  • script: scripts/ops/sentry-workflow-alerts.mjs
  • checked-in config: observability/sentry-workflow-alerts.json
  • just recipes: just sentry-alerts-list, just sentry-alerts-upsert-dry-run, just sentry-alerts-upsert
  • default org / base URL: root .sentryclircritsubi, https://sentry.io/
  • notification target: email issue owners + Active Members fallback
ローカル / CI での使い方
  1. token なしで config を確認する:
 just sentry-alerts-upsert-dry-run
  1. 現在の workflow 一覧を確認する:
 just sentry-alerts-list
  1. checked-in config を org へ反映する:
 just sentry-alerts-upsert

local では login 済みの sentry user auth、CI では alerts:write (または org:write / org:admin)を持つ SENTRY_AUTH_TOKEN を使う。 --json などの追加 option は just sentry-alerts-list --json のようにそのまま渡す。

checked-in workflow の意味
  • Production high-priority issue notifications
  • production で first_seen / regression / reappeared した issue のうち、priority が High 以上かつ level が error 以上のものを email で通知する。
  • 通知先は issue_owners、owner が無い場合は Active Members へ fallthrough する。
  • 目的: surface 横断の重大障害を SaaS UI 依存なしに再現可能な設定で維持する。
  • Production storefront vendure dependency failure spike
  • production の Storefront failure のうち dependency.name=vendurearea=vendure-fetchflow=graphqllevel >= error が揃い、1 時間で 20 件以上に達したものを通知する。
  • 目的: checkout / login / customer-context などの上流依存障害を、Sentry 上の repo 既存 vocabulary で早期検知する。
  • Production storefront /products browse issue notifications
  • production の /products browse で first_seen / regression / reappeared した issue のうち、storefront.products.route=/products かつ level >= error のものを email で通知する。
  • 通知先は issue_owners、owner が無い場合は Active Members へ fallthrough する。
  • 目的: /products browse 導線で新規障害や再発が出たときに、Slack integration へ依存せずに即時検知する。
  • Production storefront browser error boundary issue notifications
  • production の Storefront browser issue のうち、area=storefront-error-boundarystorefront.runtime=clientlevel >= error が揃うものを email で通知する。
  • 通知先は issue_owners、owner が無い場合は Active Members へ fallthrough する。
  • 目的: health / readiness では見えない client-side runtime 崩れを first_seen / regression / reappeared の時点で拾う。
  • Production react dashboard browser issue notifications
  • production の React Dashboard browser issue のうち、service=react-dashboardsurface=react-dashboardlevel >= error が揃うものを email で通知する。
  • 通知先は issue_owners、owner が無い場合は Active Members へ fallthrough する。
  • 目的: React Dashboard の browser runtime error を Vendure backend issue と切り分けて早期検知する。
運用上の注意
  • upsert は workflow 名で既存 rule を照合し、同名があれば update、無ければ create する。
  • just sentry-alerts-list / just sentry-alerts-upsert-dry-run / just sentry-alerts-upsert は local では login 済み sentry user auth、CI では SENTRY_AUTH_TOKEN を使う単一入口とする。
  • workflow description には初動で叩く just recipe を埋め込み、Sentry UI だけ開いても operator が次の切り分けコマンドへ飛べるようにする。
  • 同名 workflow が複数ある場合は fail する。UI 側で重複を整理してから再実行する。
  • 現在の ritsubi org では local user auth から Slack integration が見えていないため、repo-managed workflow の既定通知は email fallback とする。Slack を再導入する場合のみ config を差し替える。
  • AI monitoring は repo 内に対象 SDK がないため設定しない。OTel exporter は collector / routing 設計の合意が出るまで設定しない。

dashboard admin API canary(read-only contract lane)

  • script: apps/vendure-server/scripts/dashboard-api-canary.ts
  • shared logic: apps/vendure-server/src/observability/dashboard-api-canary.ts
  • Nx target: pnpm exec nx run ritsubi-vendure-server:canary:dashboard-api
  • manual just recipe: just dashboard-admin-api-smoke
  • wired into: scheduled-dashboard-smoke.yml, .github/workflows/_deploy-dashboard-workers.yml, .github/workflows/_deploy-vendure-fly.yml
ローカル / CI での使い方
  1. production の canary を手元で流す:
just dashboard-admin-api-smoke production
  1. staging の canary を手元で流す:
just dashboard-admin-api-smoke staging
canary の意味
  • products-basic
  • products list の id / name を read-only で確認する。
  • products-featured-asset
  • products list の featuredAsset { id preview } を確認する。
  • asset_translation など schema / relation drift に最も敏感な面として扱う。
  • product-variants-basic
  • productVariants list の id / sku / name を確認する。
  • customers-basic
  • customers list の id / emailAddress を確認する。
  • orders-basic
  • orders list の id / code / state を確認する。
failure 時の初動
  • canary script 自体が failure 時に次の導線を stderr へ出す:
  • just dashboard-admin-api-smoke <env>
  • just dashboard-smoke-postdeploy <env>
  • just vendure-diagnose-dashboard-products <env>
  • docs/03-implementation/infrastructure/schema-drift-runbook.md
canary 403 "missing-origin" / "disallowed-origin"(よくある障害)

症状: Sentry に Scheduled canary failed: 403 / Startup canary failed: 403 が記録される。 エラーメッセージ例: "error":"missing-origin" または "error":"disallowed-origin".

根本原因と仕組み:

canary は http://127.0.0.1:{port}/admin-apiローカルアクセスする(Cloudflare を経由しない)。 origin-validator.middleware.ts は POST 系リクエストに Origin ヘッダーを要求するが、 dashboardBaseUrl = env.ADMIN_URL が未設定の場合:

  • buildDashboardOrigin(undefined)undefined
  • canary が Origin ヘッダーなしでリクエスト送信
  • middleware: allowedOrigins 非空 + Origin なし → 403 "missing-origin"
production allowlist = [CORS_ORIGIN, STOREFRONT_URL, ADMIN_URL]
↑ ADMIN_URL が未設定だと allowlist は CORS_ORIGIN + STOREFRONT_URL のみになるが
  canary が送るべき Origin も undefined になるため 403 になる

診断コマンド:

# production の ADMIN_URL が設定されているか確認
flyctl secrets list --app ritsubi-vendure-server | grep ADMIN_URL

# staging も同様
flyctl secrets list --app ritsubi-vendure-server-staging | grep ADMIN_URL

修正: just sync-fly-secrets で deploy-targets.sh の SSOT から ADMIN_URL を再注入する (推奨)。 手動で個別設定する場合は wrapper を使う:

# 推奨: SSOT から自動注入
just sync-fly-secrets production
just sync-fly-secrets staging

# 手動 (非推奨): wrapper 経由で --stage import
bash scripts/ops/fly-secrets-set.sh -a ritsubi-ecommerce ADMIN_URL="https://dashboard.ritsubi-platform.com" --deploy
bash scripts/ops/fly-secrets-set.sh -a ritsubi-ecommerce-staging ADMIN_URL="https://dashboard-staging.ritsubi-platform.com" --deploy

設定後は canary が正常動作するかを just dashboard-admin-api-smoke <env> で確認すること。

注: ADMIN_URL は React Dashboard の Cloudflare Workers URL を指す。commerce.ritsubi-platform.com は Fly.io への DNS proxy(Worker ではない)なので、Dashboard URL は Cloudflare Workers のカスタムドメインを確認すること。

[!TIP] React Dashboard の browser smoke は runtime error / error boundary を拾うのに強く、dashboard admin API canary は contract / schema drift に強い。両方を組み合わせて false green を減らす。

storefront business-canary / KPI proxy lane

  • manual just recipes: just storefront-shadow-probe, just storefront-business-canary
  • production は shadow probe + @smoke:prod-safe、staging は shadow probe + @smoke:critical を正規 lane とする。
  • just storefront-business-canary production は read-only probe の後に authenticated Playwright smoke を流す。E2E_LOGIN_* など storefront smoke credential が AWS Secrets Manager 正本に無い場合は authenticated lane を fail させ、 shadow lane だけで green 扱いしない。default channel の business-canary は cart cleanup までに留める。
  • just storefront-login-smoke-production-domains は production のログイン専用 smoke。 scripts/ops/deploy-targets.sh に定義された canonical domain と public alias の両方で、実ブラウザのパスワードログインフォームが /api/auth-login へ POST し、303 -> /accountsession / vendure-auth-token cookie、/api/auth-session authenticated まで成立することを 確認する。Production Storefront deploy 後にログインや /commerce/shop-api のドメイン差分を疑う場合は、storefront-business-canary production 単体ではなく この recipe を先に実行する。
  • just storefront-shadow-probe <env> は health/live + readiness + version + login に加え、Shop API の readonly search / products / collections / product(slug) query を確認する。GraphQL/HTTP latency budget と minimum data threshold を超えた場合も fail する。
  • just storefront-business-canary staging は現行の staging-smoke-storefront.yml と同じく shadow lane の後に critical flow smoke を replay する。
  • just storefront-invoice-smoke staging は、認証済み Storefront から ritsOrderInvoice を実 Shop API で呼び、status=READY、署名 URL、PDF signature (%PDF-) まで確認する。既定では E2E_LOGIN_* の顧客の直近注文を使い、 明示したい場合は STOREFRONT_SMOKE_INVOICE_ORDER_CODE に同顧客が所有する確定済み注文コードを 指定する。Cloudflare Browser Rendering / R2 / 署名 URL の環境差は mock E2E では 保証できないため、請求書発行を確認したい deploy 後はこの recipe を staging で実行する。
  • just storefront-payment-purchase-smoke staging は、各決済方法でストア注文が成立することを smoke 層で一括保証する(@smoke:payment-purchase を business-canary で replay)。代引き / 売掛 / 銀行振込の非リダイレクト 3 種は常時実行し、SBPS クレジットカードは staging secret の RUN_SBPS_E2E=true のときだけ加わって 4 種になる(local では非リダイレクト 3 種のみ)。実注文を 生成するため @smoke:prod-safe を持たず production smoke では実行されない。SBPS は実課金を伴うため staging/local 限定ガードで production URL への誤実行を fail-closed で止め、完了後に同日返金する。 spec は apps/storefront/tests/e2e/smoke/payment-methods-purchase.real.spec.ts
  • shadow/business canary script は failure 時に次の導線を stderr へ出す:
  • just storefront-shadow-probe <env>
  • just storefront-business-canary <env>
  • docs/03-implementation/infrastructure/monitoring-operations.md

storefront /products performance tracing

  • 対象導線: Storefront の /products server render。
  • Sentry trace 上で確認できる custom span:
  • Storefront ProductsPage
  • Storefront Products Visible Search
  • Storefront Products Fallback Search
  • 上記 span の配下には既存の Vendure span がぶら下がる:
  • Vendure GetVisibleProductsForBrowse
  • Vendure GetProductsSimple(fallback 時)
  • trace / transaction に付与される主な tag / context:
  • storefront.products.current_page
  • storefront.products.collection
  • storefront.products.term_present
  • storefront.products.result_sourcesearch / fallback / empty
  • context storefront_products_browse
  • batch ごとの breadcrumb:
  • category: products-browse
  • message: search-batch / fallback-batch
  • data: batchIndex, rawSkip, rawTotal, sourceItems, visibleProducts, visibleTotal, pageProducts
  • raw search term は保存しない。検索有無は term_present、複数語かどうかは term_word_count で確認する。
/products が遅いときの見方

の直下で Visible SearchFallback Search のどちらが支配的か確認する。2. storefront.products.result_source=fallback が多い場合は、search index / backend の visible browse aggregation / fallback products query の順で疑う。3. products-browse breadcrumb の rawTotalpageProducts を見て、通常 browse query で十分に page が埋まっているか、fallback へ退避していないかを確認する。

storefront /search?q=... search performance tracing

  • 対象導線: Storefront の商品検索結果初回表示。
  • 計測値は /products と同じ window.__ritsubiProductsPageMetrics を使い、termPresent=true を前提に評価する。
  • 主要 artifact / smoke:
  • tests/e2e/helpers/products-page-performance.ts
  • tests/e2e/smoke/products-search-performance.real.spec.ts
  • trace / transaction では次を確認する:
  • page span: Storefront ProductsPage
  • search span: Storefront Products Visible Search
  • tag: storefront.products.term_present=true
  • breadcrumb: products-browse / search-batch
  • raw search term は Sentry に保存しない。slow trace 上では term_presentterm_word_count だけで検索有無・複数語を判断する。

storefront /products/$slug product detail performance tracing

  • 対象導線: Storefront の商品詳細初回表示。
  • Sentry trace 上で確認できる page span:
  • Storefront ProductDetailPage
  • 上記 span 配下には既存の Vendure span がぶら下がる:
  • Vendure GetProductDetailPage
  • Vendure ValidateVisibility
  • Vendure GetCoPurchaseRecommendationsPage(購入共起レコメンドがある場合)
  • trace / transaction に付与される主な tag / context:
  • storefront.product_detail.route
  • storefront.product_detail.slug
  • context storefront_product_detail
  • browser first visible は window.__ritsubiProductDetailPageMetrics に記録し、 breadcrumb product-detail:first-visible で duration / relatedProductsCount / hasWpContent を確認する。
  • 主要 artifact / smoke:
  • tests/e2e/helpers/product-detail-page-performance.ts
  • tests/e2e/smoke/product-detail-performance.real.spec.ts

shared endpoint catalog manual check

  • source catalog: observability/external-monitor-endpoints.json
  • script: scripts/ops/check-external-monitor-endpoints.mjs
  • package script: pnpm run monitor:endpoints:check
  • just recipe: just monitor-endpoints-check
  • purpose: shared catalog の status / json assertion を live endpoint に対して手動確認する。 同じ URL / method の probe は 1 回だけ fetch し、複数 assertion をまとめて評価する。
ローカルでの使い方
  1. shared catalog の全 endpoint を一発確認する:
just monitor-endpoints-check
  1. Sentry / Uptime Kuma と同じ意味論で確認する:
just monitor-endpoints-check --target sentry
just monitor-endpoints-check --target uptime-kuma
  1. 特定 endpoint だけ確認する:
pnpm run monitor:endpoints:check -- --endpoint production-vendure-schema-drift
運用上の注意
  • exit code は 1 つでも assertion が fail したら non-zero になる。
  • --target all は shared catalog 上の json check を union で評価するため、tool-specific な Sentry-only assertion(例: $.mode != "")も含めて確認したいときに使う。
  • --target sentry / --target uptime-kuma は各 tool に割り当てた json check だけを評価する。

uptime monitor automation(現在の正式運用)

  • source catalog: observability/external-monitor-endpoints.json
  • script: scripts/ops/sentry-uptime-monitors.mjs
  • just recipes: just sentry-uptime-list, just sentry-uptime-upsert
  • default org / base URL: root .sentryclircritsubi, https://sentry.io/
  • monitor scope: Production Storefront readiness, Production Vendure readiness, Production Vendure schema drift
ローカル / CI での使い方
  1. token なしで plan を確認する:
 just sentry-uptime-upsert --dry-run
  1. 現在の uptime monitor 一覧を確認する:
 just sentry-uptime-list
  1. shared catalog をそのまま org へ反映する:
 just sentry-uptime-upsert
monitor の意味
  • Production Storefront readiness
  • https://order.ritsubi-platform.com/api/health/ready を 5 分ごとに確認する。
  • 2xx に加え $.status == "ok"$.mode != "" を満たしたときのみ success とみなす。
  • 目的: production の Storefront が request を受け入れられる状態かを Sentry 上で継続監視し、trace / release と直結させる。
  • Production Vendure readiness
  • https://commerce.ritsubi-platform.com/health/ready を 5 分ごとに確認する。
  • 2xx に加え $.status == "ok" を満たしたときのみ success とみなす。
  • 目的: production の Vendure API が upstream request を受け入れられる状態かを継続監視する。
  • Production Vendure schema drift
  • https://commerce.ritsubi-platform.com/health/ready を 5 分ごとに確認する。
  • 2xx に加え $.dependencies.schemaDrift == "ok" を満たしたときのみ success とみなす。
  • 目的: production の schema drift 検知が有効であり、 dependency contract まで含めて正常かを issue / release と相関して継続監視する。
運用上の注意
  • 監視対象 endpoint の正本は observability/external-monitor-endpoints.json とし、scripts/ops/sentry-uptime-monitors.mjs が runtime に Sentry 向け定義を構築する。
  • upsert は projectSlug + environment + name で既存 monitor を照合し、同一なら update、無ければ create する。
  • just sentry-uptime-list / just sentry-uptime-upsert は local では login 済み sentry user auth、CI では SENTRY_AUTH_TOKEN を使う。
  • 403 が返る場合、script は org / project どちらの Uptime 権限が足りないかを含むメッセージを返す。毎回 API endpoint を手で辿らず、まずその診断文を確認する。
  • 現在の実装は Sentry の experimental uptime endpoint(/organizations/<org>/uptime/, /projects/<org>/<project>/uptime/)を使う。SaaS 側の互換性変更があれば script 側を更新する。
  • WordPress login と version endpoint は Sentry uptime monitor の初期セットに含めず、 scheduled-safe-probes.yml を manual fallback として残す。WordPress は Sentry project slug が DSN 依存で固定されておらず、version endpoint は availability より deploy metadata 確認の意味合いが強いため。React Dashboard は scheduled-safe-probes.yml の login probe と scheduled-dashboard-smoke.yml で補完する。

Sentry live config drift audit

  • script: scripts/ops/sentry-live-config-audit.mjs
  • just recipe: just sentry-live-config-audit
  • purpose: observability/sentry-workflow-alerts.json / observability/external-monitor-endpoints.json から導いた checked-in plan と、Sentry 上の live workflow / uptime monitor state を比較する。
ローカルでの使い方
  1. drift を JSON で確認する:
just sentry-live-config-audit --allow-drift --json
  1. drift を gate として使う:
just sentry-live-config-audit
運用上の注意
  • workflow 側は listupsert --dry-run の比較、uptime 側も同様の比較で repo-managed 設定だけを監査する。
  • suppressions の checked-in 正本は observability/sentry-live-config-audit.json とし、accepted drift の理由と issue を明記する。
  • 現在は issue #533 の billing / seat block により uptime monitor が live では disabled のままなので、現状確認だけしたい場合は --allow-drift を付ける。
  • workflow drift が missing の場合は just sentry-alerts-upsert-dry-run で planned state を確認し、必要なら observability/sentry-workflow-alerts.json の target / condition を見直す。

Uptime Kuma monitor automation(外部 watchdog)

  • source catalog: observability/external-monitor-endpoints.json
  • script: scripts/ops/uptime-kuma-monitors.mjs
  • target instance: https://milestone-monitoring.exe.xyz
  • public status page: https://ritsubi-monitoring.exe.xyz/status/commerce
  • hosting note: Uptime Kuma は エリアルのサーバー内にホストされている。外部 SaaS と同じ前提で扱わず、疎通・認証・header 要件はその配置を前提に確認する。
  • monitor scope: Production Storefront readiness, Production Vendure readiness, Production Vendure schema drift, Production WordPress login
ローカル / CI での使い方
  1. live auth なしで plan と payload を確認する:
pnpm run uptime:kuma -- upsert --dry-run
  1. 現在の Uptime Kuma monitor 一覧を確認する:
pnpm run uptime:kuma -- list
  1. shared catalog をそのまま Uptime Kuma へ反映する:
pnpm run uptime:kuma -- upsert
monitor の意味
  • Production Storefront readiness
  • https://order.ritsubi-platform.com/api/health/ready を 5 分ごとに確認する。
  • Uptime Kuma の json-query monitor として $.status == "ok" を満たしたときのみ success とみなす。
  • 目的: Storefront の request 受付可否を、Sentry とは独立した watchdog でも継続監視する。
  • Production Vendure readiness
  • https://commerce.ritsubi-platform.com/health/ready を 5 分ごとに確認する。
  • Uptime Kuma の json-query monitor として $.status == "ok" を満たしたときのみ success とみなす。
  • 目的: Vendure API の readiness を外部監視として継続確認する。
  • Production Vendure schema drift
  • https://commerce.ritsubi-platform.com/health/ready を 5 分ごとに確認する。
  • Uptime Kuma の json-query monitor として $.dependencies.schemaDrift == "ok" を満たしたときのみ success とみなす。
  • 目的: schema drift 検知が ok であることを外部監視として継続確認し、 schemaDrift dependency が欠落したケースも down として扱う。
  • Production WordPress login
  • https://cms.ritsubi-platform.com/wp-login.php を 5 分ごとに確認する。
  • HTTP 200 を success とみなす。
  • 目的: WordPress の public login surface の到達性を継続確認する。
運用上の注意
  • 監視対象 endpoint の正本は observability/external-monitor-endpoints.json とし、scripts/ops/uptime-kuma-monitors.mjs が runtime に Kuma 向け定義を構築する。
  • Uptime Kuma への反映は JSON ファイルの native import ではなく、 scripts/ops/uptime-kuma-monitors.mjs が shared catalog から定義を組み立てて Socket.IO 管理 API へ upsert する。
  • upsert は monitor 名で既存定義を照合し、同名があれば update、無ければ create する。
  • config に notificationIDList を明示しない限り、既存 monitor の通知アタッチメントは維持する。
  • UPTIME_KUMA_TOKEN を優先し、代替として UPTIME_KUMA_USERNAME / UPTIME_KUMA_PASSWORD を使う。
  • milestone-monitoring.exe.xyz は exe.dev ログイン配下のため、CLI からは追加 header が必要になる場合がある。その場合は UPTIME_KUMA_HEADERS_JSON を使うか、到達可能な内部 URL に UPTIME_KUMA_URL を切り替える。
  • さらに、この Uptime Kuma 自体は エリアルのサーバー内にホストされている。monitor apply や接続障害の切り分けでは、対象 endpoint 側だけでなく、エリアル側サーバーからの疎通 / 認証条件 / ingress 変更有無も先に確認する。
  • React Dashboard は専用 health endpoint が無いため Uptime Kuma monitor 初期セットには含めない。代わりに scheduled-safe-probes.yml の login probe と scheduled-dashboard-smoke.yml で surface を補完する。
  • /api/health/cms と version endpoint は診断 / metadata 用途なので継続監視に使わない。

Surface 別の実際の挙動

  • Storefront
  • workflow: .github/workflows/_deploy-storefront-workers.yml
  • release: storefront@<sha>
  • sourcemap source: apps/storefront/dist
  • deploy 前に .map を除去するため、公開 artifact へ sourcemap を残さない。
  • cloudflare-deploy.mjsSENTRY_ENVIRONMENT / VITE_PUBLIC_SENTRY_ENVIRONMENTSENTRY_RELEASE / VITE_PUBLIC_SENTRY_RELEASE を Wrangler 設定へ注入する。
  • deploy workflow は SENTRY_DSN / VITE_PUBLIC_SENTRY_DSN / SENTRY_AUTH_TOKEN が欠けると fail-fast する。
  • browser 側の feedback は @sentry/react の既存 client init 上で有効化し、 Userback は使用しない。
  • Vendure
  • workflow: .github/workflows/_deploy-vendure-fly.yml
  • release: vendure@<sha>
  • release / commit association は自動化済み。
  • production は deploy 成功後に apps/vendure-server/dist を CI 上で build:production し、debug-id 注入後に sourcemap を upload する。
  • staging は tsconfig.build.json で生成した TypeScript source map を Fly image の NODE_OPTIONS=--enable-source-maps で runtime 解決する。
  • Fly secret validation は SENTRY_DSN を必須化している。
  • recurring task のうち SMILE export / cleanup は withMonitor(...) ベースの monitor slug (vendure-smile-export-orders, vendure-smile-export-cleanup) を付与している。
  • worker process は vendure-worker-heartbeat-production / vendure-worker-heartbeat-staging の interval monitor で 5 分ごとに check-in する。
  • @sentry/profiling-node を trace lifecycle 連動で有効化し、trace sample が 0 より大きいときだけ Profile を生成する。
  • React Dashboard
  • workflow: .github/workflows/_deploy-dashboard-workers.yml
  • release: dashboard@<sha>
  • sourcemap source: apps/vendure-server/dist/dashboard
  • build-time に VITE_SENTRY_*VITE_ADMIN_API_URL を注入し、browser SDK を初期化する。VITE_SENTRY_DSN は明示必須で、shared/runtime 用 SENTRY_DSN への暗黙 fallback はしない。
  • deploy 前に .map を除去せず、CI から sourcemap upload 後に Worker へ配備する。
  • browser 側の feedback は @sentry/react の既存 init に統合している。
  • admin API request ごとに x-request-id を付与し、Vendure backend の request_id tag と合わせて browser event / Replay を相関させる。
  • auth payload (me.id, activeAdministrator.id) から actor_id, actor_type, dashboard_admin context を更新する。
  • WordPress CMS
  • env source: staging_wp / production_wp
  • release: wordpress@<sha> を推奨。未設定時は release なしで event を送る。
  • runtime は ritsubi-ec-plugin/includes/sentry.php が bootstrap し、fatal error / uncaught exception を Sentry store endpoint へ直接送信する。
  • browser SDK と sourcemap upload は未導入のため、WordPress 直描画ページの JavaScript error は現時点では対象外。

staging / production での確認手順

共通の確認手順

  1. 対応する GitHub Actions deploy job の summary で、 Prepare Sentry release contextFinalize ... Sentry release が成功していることを確認する。前段の validate step で required Sentry config が通っていることも確認する。
  2. Sentry Releases で対象 release(storefront@<sha> / vendure@<sha> / dashboard@<sha>)が作成済みで、commits が関連付いていることを確認する。
  3. Event Details の environmentrelease が deploy 対象と一致することを確認する。
  4. Sentry Uptime Monitors で Production Storefront readiness / Production Vendure readiness の latest check が success になっていることを確認する。

Storefront の確認手順

  • staging / production のブラウザまたは server-side でイベントを開き、 storefront_request / storefront_customer context と GraphQL breadcrumb が入っていることを確認する。
  • API / client failure の triage では、まず tag area, flow, operation, route を見て、どの surface で失敗したかを絞る。現在は /api/campaigns/api/auth-bypass/api/customer-hierarchycustomer-contextconsent-context、TanStack Query cache clear on logout failure がこの導線に乗る。
  • storefront_request context の requestId, pathname, authState と、event extra の surface 固有情報(例: limit, shippingMode, consent 系の追加情報)を GitHub Actions artifact / アプリログと突合する。
  • Storefront の Feedback widget は常時表示しない。関係者が ?sentry-feedback=1 を付けて開き、Sentry Feedback の標準 UI が表示されることを manual に確認する。name / email field は非表示でよい。
  • staging では staging-sentry-smoke-storefront.yml が non-production 専用の debug route を使って intentional debug event を 1 件発生させ、run marker 付き diagnostics bundle を artifact 化する。triage 時は artifact の summary.mdmatched-events.json から対象 event を開く。
  • non-production では次のどちらかで疎通確認できる。
  • GET/POST /api/debug-sentryx-sentry-debug-token を付けて 500 を発生させる。scheduled canary は SENTRY_DEBUG_SIGNING_SECRET と marker から導出した token を使い、手動 debug 用の static SENTRY_DEBUG_TOKEN とは分離する。
  • POST /api/client-logs{"message":"__SENTRY_DEBUG__","context":{"sentryDebug":true}} を送り、browser 系の debug event を発生させる。
  • production では上記 debug route は 404 / 無効化が正しい。
  • POST /monitoring(Storefront Sentry tunnel)は browser tunnel transport のため production でも残すが、same-origin request を必須とする。

Vendure の確認手順

  • deploy workflow の Verify deployed version metadata が通っていることを確認する。これにより /version の commit metadata が deploy 対象 SHA と一致している。
  • Sentry event では request, graphql, customer, channel, vendure context が確認対象。transaction 名は <api-type>-api <operation-type> <operation-name-or-field> 形式になる。
  • uncaughtException / unhandledRejection による fatal crash でも、共通の process handler が captureException()flushSentry()process.exit(1) の順で送信する。request context を持たない restart 直前の障害は、この fatal event を起点に見る。
  • backend stack trace は TypeScript の file/line を解決できることを確認する。
  • Sentry Monitors / Crons で vendure-smile-export-ordersvendure-smile-export-cleanup の check-in が入っていることを確認する。
  • worker monitor では vendure-worker-heartbeat-production または vendure-worker-heartbeat-staging の latest check-in が継続して success になっていることを確認する。
  • slow transaction を開き、Profile タブから CPU profile が添付されていることを確認する。

React Dashboard の確認手順

  • Cloudflare Workers deploy 後に browser event / Replay を確認し、 release=dashboard@<sha>、tag service=react-dashboardsurface=react-dashboardapi_type=admin が入ることを確認する。
  • admin API 起点の event では tag request_id, actor_id, actor_type とcontext dashboard_request, dashboard_admin, dashboard_recent_requests が入ることを確認する。
  • 同じ request_id で Vendure backend event を検索し、browser 側の Replay / event と backend 側の request 文脈を相互に辿れることを確認する。
  • イベントが来ない場合は、build-time に VITE_SENTRY_DSNVITE_ADMIN_API_URL が明示注入されていたかを最初に確認する。SENTRY_DSN だけでは browser SDK は有効化されない。
  • Feedback widget を開き、React Dashboard 上でも Sentry Feedback が表示されることを確認する。

Sentry 上で読める業務コンテキスト

Storefront のメトリクス

  • tags: storefront.runtime, storefront.route, storefront.auth_state, storefront.customer_status, storefront.customer_role, storefront.sentry_debug_marker, area, flow, operation, route
  • contexts: storefront_request(runtime, requestId, pathname, authState)、 storefront_customer(customerCode, billingCustomerCode, customerStatus, roleType)、storefront_debug(marker)
  • breadcrumbs: graphql category に operation 名、requestId、pathname、authMechanism、cache、result
  • extras: API / context / vendureQueryClient ごとの補助情報(例: limit, shippingMode, consent request metadata など)

Vendure のメトリクス

  • tags: actor_id, actor_type, api_type, channel_id, customer_code, graphql_field, graphql_operation, graphql_operation_type, request_id, job_kind, job_name, job_monitor_slug
  • contexts: actor, channel, customer, graphql, request, vendure
  • recurring task monitor では vendure_job context に schedule / description / channel などが入る。
  • plugin から setSentryPluginContext() を使う場合、 plugin.<name> context と plugin_<name>_* tag が追加される。

React Dashboard のメトリクス

  • tags: service, surface, api_type, request_id, actor_id, actor_type
  • contexts: dashboard_request(id, method, path, statusCode, failed)、 dashboard_admin(userId, administratorId)、 dashboard_recent_requests(直近 5 件の admin API request)
  • Storefront / Vendure ほどの customer/channel enrichment はまだ入れていないが、Vendure admin API とは request_id を軸に相関できる。

アラート / トリアージ / ownership の基本

事象 主担当 最初に見る項目 次の確認先
Storefront browser / edge / server error Storefront 担当 area, flow, operation, route, storefront_request, storefront_customer, GraphQL breadcrumb Cloudflare deploy SHA、Storefront logs
Vendure GraphQL / API error Vendure 担当 api_type, channel_id, graphql_operation, customer_code, request_id Fly deploy SHA、/version metadata、Vendure logs

Safe probing tiers

監視エンドポイントは用途によって 2 つのカテゴリに分かれる。

  • Safe Probe(Tier 0) ── アプリ自身の稼働状態のみを返し、外部サービスへの fan-out を一切行わない。外形監視ツールや CI からの継続的な死活確認、Fly.io / Cloudflare の health check に使える。
  • CMS 診断エンドポイント(Tier 3) ── CMS 配信経路全体(Vendure 経由の WordPress 疎通を含む)を検査する。呼び出しごとに upstream への fan-out が発生するため、連続ポーリングには使わない。「Safe Probe は全て正常なのに CMS コンテンツが表示されない」という場面で、障害箇所(Vendure 側 / WordPress 側 / 両方)を切り分けるためにワンショットで呼び出す診断専用エンドポイント。

オペレーター向け早見表: どのエンドポイントを使うか

確認したいこと 使うエンドポイント
アプリが起動しているか(外形監視 / Fly.io health check) GET /api/health/live(または Vendure /health/live
デプロイ後にトラフィックを受け入れられるか GET /api/health/ready(または Vendure /health/ready
今デプロイされているバージョンは何か GET /api/version(または Vendure /version
CMS コンテンツが表示されない原因を調べたい(診断) GET /api/health/cmsワンショットのみ

  • Tier 0 Safe Probe: vendure /health/live, vendure /health/ready, vendure /version, storefront /api/health/live, storefront /api/health/ready, storefront /api/version。外部依存の fan-out がなく継続監視に適したエンドポイント。production の継続監視は observability/external-monitor-endpoints.json を正本に、 scripts/ops/sentry-uptime-monitors.mjs が Sentry Uptime へ、 scripts/ops/uptime-kuma-monitors.mjs が Uptime Kuma へ反映する。Sentry は Storefront readiness / Vendure readiness / Vendure schema drift、Uptime Kuma は Storefront readiness / Vendure readiness / Vendure schema drift / WordPress login を 5 分ごとに監視する。staging・version endpoint・WordPress admin redirect は .github/workflows/scheduled-safe-probes.yml に残し、manual fallback として実行する。fallback workflow は HTTP 200 だけでなく レスポンスボディの shape も検証する(health 系は status == "ok"、version 系は versioncommitbuildTimeenv フィールドの存在と非空文字列を確認)。fallback が失敗した場合、SLACK_WEBHOOK_URL シークレットが設定されていれば notify-slack-webhook action 経由で Slack に通知される。
  • Tier 1 Deploy Verification: apps/vendure-server/scripts/post-deploy-check.sh の schema / CORS / asset delivery と deploy workflow の version verify
  • Tier 2 Synthetic Journey: staging-sentry-smoke-storefront.ymlstaging-smoke-storefront.yml の browser canary、 および production-sentry-smoke-storefront.yml の manual observability 切り分け。いずれかの smoke job が失敗した場合も同様に Slack へ通知される。
  • Tier 2.5 Sentry diagnostics snapshot: staging-smoke-storefront.yml は failure / cancelled 時に deployed release を基準に Sentry issue / event snapshot を artifact 化する。 staging-sentry-smoke-storefront.yml は staging 専用 debug event の run marker に一致する event を取得し、summary.md / matched-events.json を triage の最短導線とする。
  • Tier 3 Diagnostic / Internal Probe: storefront /api/health/cms「CMS 配信経路のどこが壊れているか」を特定するための診断専用エンドポイント。Vendure CMS API と WordPress upstream 直疎通の 2 系統を個別にチェックし、障害箇所を root-cause レベルで切り分けるための情報を返す。upstream への fan-out を含むため、継続ポーリングには使わない。障害調査・デプロイ後のワンショット確認のみ。レスポンスの top-level status フィールドは ok | degraded | fail を返す(HTTP 503: 両チェック失敗 / HTTP 200 + degraded: 片方のみ失敗)。詳細は 5.2 Storefront ヘルスチェックエンドポイント を参照。

運用ルール

  • まず releaseenvironment で「どの配備から発生したか」を確定する。
  • request_id があるイベントは、アプリログ側の同一 request と突合する。
  • Storefront の Vendure failure は graphql.error_code / graphql.operation_type / http.status_code / dependency.name を優先して見て、auth/validation/upstream failure を切り分ける。
  • customer_code / storefront_customer.customerCode / channel_id がある場合は blast radius 判定に使う。
  • checkout / login / order 系で production に継続影響がある場合は、error 数よりも business impact を優先して即時エスカレーションする。

現状確定の観測サマリ

過去ここにあった「将来 enrichment する / threshold tuning する / surface を広げる」系の deferred items は docs/04-project-management/architecture-improvement-backlog.md の「Observability backlog」へ移動した。ここには 現状確定の運用形 だけを残す。

  • Sentry Logs限定採用 とし、repo の system of record は既存の構造化 stdout ログを維持する。Vendure は Pino → stdout、Storefront / React Dashboard は platform 側の stdout / browser console を正本としつつ、JS runtime では enableLogs: true と warn/error の consoleLoggingIntegration を有効化して issue / trace / feedback / replay と相関できるようにする。
  • Storefront の Vendure GraphQL / HTTP failure は storefront.graphql_operation / graphql.operation_type / graphql.error_code / http.status_code / dependency.name=vendure を Sentry tag/context に残し、checkout / login / order 障害の切り分けを初動から可能にする。
  • Storefront / React Dashboard の browser surface は conservative な beforeSend を使い、browser extension 由来ノイズを drop しつつ authorization / cookie / token などの sensitive field を event / request / breadcrumb から redact する。
  • browser surface(Storefront / React Dashboard)では httpClientIntegration() も有効化し、既定の 5xx failed request を Sentry event として追跡する。4xx を含める必要が出た場合のみ、ノイズとコストを見て別途拡張する。
  • Storefront Session Replay は browser client init で有効。sample rate は VITE_PUBLIC_SENTRY_REPLAYS_SESSION_SAMPLE_RATEVITE_PUBLIC_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE で調整し、production では既定で session 10% / on-error 100% を使う。
  • React Dashboard Session Replay も browser init で有効。sample rate はコード定数(production: session 10%、non-production: session 100%、on-error 100%)を使い、VITE_ADMIN_API_URL を正本に trace propagation target と x-request-id 相関を構成する。
  • Sentry AI monitoring は現時点で非適用。repo 内に OpenAI / Anthropic / LangChain / Vercel AI SDK などの AI SDK 導線が入った時点で再評価する。
  • Storefront Worker OTel exporter は active。apps/storefront/wrangler.tomlobservability.logs.destinations / observability.traces.destinations を repo 管理の正本とし、Cloudflare dashboard 側で同名 destination を Sentry の OTLP traces/logs endpoint へ向ける。
  • Storefront runtime bootstrap telemetry は mandatory。 sentry.server.config.ts / sentry.edge.config.ts の初期化完了時に「app code まで到達した」ことを build metadata (version, release, commit, deployTarget) 付きで構造化ログに残し、 Cloudflare 1101 のような Worker 初期化失敗とアプリ内 500 を切り分ける。
  • Storefront request guard failure telemetry は mandatory。 server-auth-guard.tsserver-maintenance-guard.ts は、予期しない例外を未捕捉のまま上げず、requestId, traceId, route, guard, fallback を含む構造化ログと Sentry capture を残して degrade する。
  • browser SDK / Replay / feedback は @sentry/react、Worker SDK は @sentry/cloudflare で運用し、Cloudflare Observability は Worker runtime telemetry の配送面を担う。
  • collector / routing infrastructure を新設する判断や、Prometheus / Grafana / Loki を Fly.io で常時運用する実需要が出るまでは、現行の Sentry + Cloudflare Observability export + Uptime Kuma + manual fallback を正規運用とする。

[!NOTE] 以下の /metrics、Prometheus、Grafana に関する例は、アプリ側の計測仕様と将来導入用テンプレートを兼ねる。現時点ではこれらを Fly.io の常設監視 app として運用しない。

2. インフラストラクチャ監視

2.1 Fly.io メトリクス

システムリソース監視

// system-metrics.ts
import { register, Gauge } from "prom-client";
import { execSync } from "child_process";

export class SystemMetrics {
  private cpuUsageGauge = new Gauge({
    name: "system_cpu_usage_percent",
    help: "CPU usage percentage",
  });

  private memoryUsageGauge = new Gauge({
    name: "system_memory_usage_bytes",
    help: "Memory usage in bytes",
  });

  private diskUsageGauge = new Gauge({
    name: "system_disk_usage_bytes",
    help: "Disk usage in bytes",
    labelNames: ["mount"],
  });

  constructor() {
    register.registerMetric(this.cpuUsageGauge);
    register.registerMetric(this.memoryUsageGauge);
    register.registerMetric(this.diskUsageGauge);

    // 30秒ごとにメトリクス更新
    setInterval(() => this.updateMetrics(), 30000);
  }

  private updateMetrics() {
    try {
      // CPU使用率
      const cpuUsage = this.getCPUUsage();
      this.cpuUsageGauge.set(cpuUsage);

      // メモリ使用量
      const memoryUsage = this.getMemoryUsage();
      this.memoryUsageGauge.set(memoryUsage);

      // ディスク使用量
      const diskUsage = this.getDiskUsage();
      Object.entries(diskUsage).forEach(([mount, usage]) => {
        this.diskUsageGauge.labels(mount).set(usage);
      });
    } catch (error) {
      console.error("Failed to update system metrics:", error);
    }
  }

  private getCPUUsage(): number {
    try {
      const output = execSync("top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1");
      return parseFloat(output.toString().trim());
    } catch {
      return 0;
    }
  }

  private getMemoryUsage(): number {
    try {
      const output = execSync("free -b | grep '^Mem:' | awk '{print $3}'");
      return parseInt(output.toString().trim());
    } catch {
      return 0;
    }
  }

  private getDiskUsage(): Record<string, number> {
    try {
      const output = execSync("df -B1 | tail -n +2");
      const lines = output.toString().trim().split("\n");
      const usage: Record<string, number> = {};

      lines.forEach((line) => {
        const parts = line.split(/\s+/);
        if (parts.length >= 6) {
          const mount = parts[5];
          const used = parseInt(parts[2]);
          usage[mount] = used;
        }
      });

      return usage;
    } catch {
      return {};
    }
  }
}

2.2 データベース監視

PostgreSQL 監視

// database-metrics.ts
export class DatabaseMetrics {
  private connectionPoolGauge = new Gauge({
    name: "postgres_connection_pool_size",
    help: "PostgreSQL connection pool size",
    labelNames: ["state"],
  });

  private queryDurationHistogram = new Histogram({
    name: "postgres_query_duration_seconds",
    help: "PostgreSQL query duration",
    labelNames: ["operation"],
    buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
  });

  private activeConnectionsGauge = new Gauge({
    name: "postgres_active_connections",
    help: "Number of active PostgreSQL connections",
  });

  constructor(private dataSource: DataSource) {
    register.registerMetric(this.connectionPoolGauge);
    register.registerMetric(this.queryDurationHistogram);
    register.registerMetric(this.activeConnectionsGauge);

    // 接続プール監視
    setInterval(() => this.updateConnectionMetrics(), 30000);
  }

  trackQuery(operation: string, duration: number) {
    this.queryDurationHistogram.labels({ operation }).observe(duration / 1000);
  }

  private async updateConnectionMetrics() {
    try {
      if (this.dataSource.isInitialized) {
        const pool = (this.dataSource.driver as any).master;

        if (pool) {
          this.connectionPoolGauge.labels("total").set(pool.totalCount || 0);
          this.connectionPoolGauge.labels("idle").set(pool.idleCount || 0);
          this.connectionPoolGauge.labels("waiting").set(pool.waitingCount || 0);
        }

        // アクティブ接続数を取得
        const result = await this.dataSource.query(
          "SELECT count(*) as active_connections FROM pg_stat_activity WHERE state = $1",
          ["active"],
        );

        this.activeConnectionsGauge.set(parseInt(result[0]?.active_connections || "0"));
      }
    } catch (error) {
      console.error("Failed to update database metrics:", error);
    }
  }
}

Redis 監視

// redis-metrics.ts
export class RedisMetrics {
  private connectionGauge = new Gauge({
    name: "redis_connected_clients",
    help: "Number of connected Redis clients",
  });

  private memoryUsageGauge = new Gauge({
    name: "redis_memory_usage_bytes",
    help: "Redis memory usage in bytes",
  });

  private commandsProcessedTotal = new Counter({
    name: "redis_commands_processed_total",
    help: "Total number of commands processed",
    labelNames: ["command"],
  });

  private keyspaceGauge = new Gauge({
    name: "redis_keyspace_keys",
    help: "Number of keys in Redis keyspace",
    labelNames: ["db"],
  });

  constructor(private redis: Redis) {
    register.registerMetric(this.connectionGauge);
    register.registerMetric(this.memoryUsageGauge);
    register.registerMetric(this.commandsProcessedTotal);
    register.registerMetric(this.keyspaceGauge);

    // Redis INFO 監視
    setInterval(() => this.updateRedisMetrics(), 30000);
  }

  private async updateRedisMetrics() {
    try {
      const info = await this.redis.info();
      const sections = this.parseRedisInfo(info);

      // 接続数
      if (sections.clients?.connected_clients) {
        this.connectionGauge.set(parseInt(sections.clients.connected_clients));
      }

      // メモリ使用量
      if (sections.memory?.used_memory) {
        this.memoryUsageGauge.set(parseInt(sections.memory.used_memory));
      }

      // キースペース
      Object.entries(sections.keyspace || {}).forEach(([db, info]) => {
        const match = info.match(/keys=(\d+)/);
        if (match) {
          this.keyspaceGauge.labels(db).set(parseInt(match[1]));
        }
      });
    } catch (error) {
      console.error("Failed to update Redis metrics:", error);
    }
  }

  private parseRedisInfo(info: string): Record<string, Record<string, string>> {
    const sections: Record<string, Record<string, string>> = {};
    let currentSection = "";

    info.split("\n").forEach((line) => {
      line = line.trim();
      if (line.startsWith("#")) {
        currentSection = line.substring(2).toLowerCase();
        sections[currentSection] = {};
      } else if (line.includes(":")) {
        const [key, value] = line.split(":");
        if (sections[currentSection]) {
          sections[currentSection][key] = value;
        }
      }
    });

    return sections;
  }
}

3. ログ管理

3.1 構造化ログ

現在の実装では、構造化ログは runtime ごとに別 schema を持つのではなく、共通の相関 field を揃えた JSON を正本とする。

  • 共通 join key:
  • requestId
  • traceId
  • spanId
  • surface
  • source
  • Storefront / browser log ingest / Vendure logger / Playwright manifest / workflow archive は、同じ field 名で相関する。
  • Vendure logger は pino を使い、message key を正本とする。
  • Vendure の logger child binding は requestId, traceId, spanId, operationName, operationType, fieldName, apiType, channelId, customerCode, actorType を保持する。

運用時にどの artifact / log / Sentry event をどの順で開くかは Observability 相関ガイド を参照する。

3.2 ログ集約設定

# fluentd-config.yml (オプション)
<source> @type tail path /app/logs/*.log pos_file
/var/log/fluentd-vendure.log.pos tag vendure.* format json time_key timestamp
time_format %Y-%m-%dT%H:%M:%S.%LZ </source>

<match vendure.**> @type datadog api_key "#{ENV['DD_API_KEY']}" service vendure
source nodejs sourcecategory vendure tags
environment:#{ENV['NODE_ENV']},instance:#{ENV['FLY_MACHINE_ID']} </match>

3.3 Storefront ログ方針(構造化 JSON + stdout)

Storefront(Vite)は アプリ側で構造化した JSON ログを stdout に出力する方針とする。
stdout はローカル開発や実行環境での標準出力として扱い、アプリ側は JSON の整形と共通フィールド付与に集中する。
本番の Storefront は Cloudflare 上で稼働するため、ログ管理は Cloudflare 側で行う。

目的

  • 画面/リクエスト単位で追跡できるログフォーマットに統一する
  • Cloudflare / ローカル stdout の両方で追跡しやすい形にそろえる

基本設計

  • 現在の実装は apps/storefront/src/lib/logger.ts の console-based logger で、JSON 形式のログを stdout に出力する
  • 共通フィールドを付与する(以下の項目を標準とする)
  • PII はログに残さない(必要な場合はマスク処理を経由する)

共通フィールド(標準)

必須

  • service: サービス名(例: storefront / vendure
  • environment: 実行環境(例: local / staging / production
  • level: ログレベル(info / warn / error など)
  • message: ログ本文
  • timestamp: ISO8601

推奨

  • requestId: リクエスト単位の相関ID
  • route: ルートまたはパス
  • method: HTTP メソッド
  • status: HTTP ステータス(レスポンス時)
  • durationMs: レイテンシ(ミリ秒)

禁止(PII)

  • 氏名・メール・電話など直接個人を特定できる値
  • 生のトークン・セッション・認可情報

実装方針

  • アプリ側で構造化し、stdout に出力する(転送レイヤの変換は最小限)
  • Storefront / Vendure で共通の logger ラッパーを用意し、必須フィールドを自動付与する
  • stdout を system of record としつつ、warn/error の console ログは Sentry Logs にも転送し、error / trace / replay / feedback との相関を取りやすくする
  • Storefront の warn/error logger と browser client log は、 requestId / route / source / service / environment を含む Sentry breadcrumb も残し、issue 側から同一 request の stdout / client log を辿りやすくする

4. アラート設定

4.1 クリティカルアラート

// alert-rules.ts
export const alertRules = {
  // アプリケーション可用性
  application_down: {
    metric: "up",
    condition: "== 0",
    duration: "2m",
    severity: "critical",
    message: "Vendure application is down",
    channels: ["slack", "email", "pagerduty"],
  },

  // レスポンス時間
  high_response_time: {
    metric: "vendure_api_duration_seconds",
    condition: "> 2",
    duration: "5m",
    severity: "warning",
    message: "API response time is high (>2s)",
    channels: ["slack"],
  },

  // エラー率
  high_error_rate: {
    metric: 'rate(vendure_api_requests_total{status=~"5.."}[5m])',
    condition: "> 0.05",
    duration: "3m",
    severity: "critical",
    message: "High error rate detected (>5%)",
    channels: ["slack", "email"],
  },

  // データベース接続
  database_connection_high: {
    metric: "postgres_active_connections",
    condition: "> 80",
    duration: "5m",
    severity: "warning",
    message: "High database connection count",
    channels: ["slack"],
  },

  // Redis メモリ使用量
  redis_memory_high: {
    metric: "redis_memory_usage_bytes",
    condition: "> 2.5e9", // 2.5GB
    duration: "5m",
    severity: "warning",
    message: "Redis memory usage is high",
    channels: ["slack"],
  },

  // ディスク使用量
  disk_usage_high: {
    metric: "system_disk_usage_bytes",
    condition: "> 0.85",
    duration: "10m",
    severity: "warning",
    message: "Disk usage is high (>85%)",
    channels: ["slack"],
  },

  // ビジネス指標
  order_failure_rate: {
    metric: "rate(vendure_orders_failed_total[10m])",
    condition: "> 0.1",
    duration: "5m",
    severity: "critical",
    message: "Order failure rate is high (>10%)",
    channels: ["slack", "email", "business_team"],
  },
};

4.2 アラート通知設定

// notification-channels.ts
export class NotificationManager {
  private channels: Map<string, NotificationChannel> = new Map();

  constructor() {
    this.setupChannels();
  }

  private setupChannels() {
    // Slack 通知
    this.channels.set(
      "slack",
      new SlackChannel({
        webhookUrl: process.env.SLACK_WEBHOOK_URL!,
        channel: "#vendure-alerts",
        username: "Vendure Monitor",
      }),
    );

    // Email 通知(SES)
    this.channels.set(
      "email",
      new EmailChannel({
        ses: {
          region: process.env.AWS_REGION!,
          fromAddress: process.env.EMAIL_FROM!,
        },
        from: "alerts@ritsubi.co.jp",
        to: ["admin@ritsubi.co.jp", "dev@ritsubi.co.jp"],
      }),
    );

    // PagerDuty 通知(クリティカル用)
    this.channels.set(
      "pagerduty",
      new PagerDutyChannel({
        integrationKey: process.env.PAGERDUTY_INTEGRATION_KEY!,
      }),
    );
  }

  async sendAlert(alert: Alert) {
    const promises = alert.channels.map(async (channelName) => {
      const channel = this.channels.get(channelName);
      if (channel) {
        try {
          await channel.send(alert);
        } catch (error) {
          console.error(`Failed to send alert to ${channelName}:`, error);
        }
      }
    });

    await Promise.allSettled(promises);
  }
}

interface Alert {
  severity: "info" | "warning" | "critical";
  title: string;
  message: string;
  timestamp: Date;
  channels: string[];
  metadata?: any;
}

interface NotificationChannel {
  send(alert: Alert): Promise<void>;
}

class SlackChannel implements NotificationChannel {
  constructor(
    private config: {
      webhookUrl: string;
      channel: string;
      username: string;
    },
  ) {}

  async send(alert: Alert): Promise<void> {
    const color = {
      info: "#36a64f",
      warning: "#ff9900",
      critical: "#ff0000",
    }[alert.severity];

    const payload = {
      channel: this.config.channel,
      username: this.config.username,
      attachments: [
        {
          color,
          title: alert.title,
          text: alert.message,
          timestamp: Math.floor(alert.timestamp.getTime() / 1000),
          fields: [
            {
              title: "Severity",
              value: alert.severity.toUpperCase(),
              short: true,
            },
            {
              title: "Environment",
              value: process.env.NODE_ENV || "unknown",
              short: true,
            },
          ],
        },
      ],
    };

    const response = await fetch(this.config.webhookUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });

    if (!response.ok) {
      throw new Error(`Slack notification failed: ${response.statusText}`);
    }
  }
}

5. ヘルスチェック

5.1 Vendure ヘルスチェックエンドポイント

Vendure が公開するヘルスチェック / 監視エンドポイントの一覧と用途:

エンドポイント 用途 連続ポーリング
GET /health/live Liveness probe。プロセスが応答できるかだけを確認(外部依存なし)。Fly.io の TCP/HTTP health check に使う。 ✅ 可
GET /health/ready Readiness probe。DB・Redis 接続を確認し、トラフィック受け入れ可否を返す。デプロイ後ポーリングに使う。 ✅ 可
GET /health /health/ready と同等の後方互換エイリアス。新規ワークフローでは /health/ready を使うこと。 ✅ 可(非推奨)
GET /version デプロイ済みの git連動 build version / release version / commit SHA を返す。デプロイ検証ステップで使用。 ✅ 可
GET /metrics Node.js ランタイムメトリクス(JSON形式)。Fly.io ヘルスチェックおよび ad-hoc 確認用。 ✅ 可

Tier 0 Safe Probe の自動実行: .github/workflows/scheduled-safe-probes.yml が staging・production の Tier 0 エンドポイントをスケジュール実行している。Vendure / Storefront については HTTP 200 に加えてレスポンスボディの shape(status == "ok" / version フィールドの存在)を検証し、WordPress については wp-login.php200post-new.php?post_type=product_detail の未ログイン時 302 / 303 を確認する。失敗時は Slack 通知ジョブが起動する。

Storefront(Vite / Cloudflare Workers)が公開する監視・診断エンドポイントの一覧と用途。エンドポイントの選択指針は Safe probing tiers の早見表も参照。

エンドポイント 用途 連続ポーリング
GET /api/health/live Liveness probe。Worker プロセスが応答できるかのみ確認(外部依存なし)。 ✅ 可
GET /api/health/ready Readiness probe。依存サービスへの基本疎通を確認し、トラフィック受け入れ可否を返す。 ✅ 可
GET /api/version デプロイ済みの git連動 build version / release version / commit SHA を返す。デプロイ検証に使用。 ✅ 可
GET /version GET /api/version のエイリアス。Safe probe や手動確認時に短いパスで参照したい場合に使用。 ✅ 可
GET /api/health/cms CMS 配信経路診断。production は status-only、non-production は詳細 message を返す。 ❌ 診断専用
POST /monitoring Sentry browser tunnel。same-origin リクエストのみ受け入れる(Sec-Fetch-Site 検証)。外部からのアクセスは 403。 ❌ ブラウザ専用

/api/health/cms を Safe Probe と分けている理由

Safe Probe(/api/health/live, /api/health/ready, /api/version)はアプリ自身の稼働状態のみを返し、外部サービスへのリクエストを一切発生させない。一方、/api/health/cms は呼び出しごとに Vendure CMS API(Fly.io 内部)および WordPress(外部 upstream)への fan-out リクエストを発生させる。

この fan-out を連続ポーリングに使うと次の問題が生じる。

  • upstream(Vendure / WordPress)に不必要な負荷がかかる
  • upstream の一時的な遅延が誤検知アラートを引き起こす
  • Safe Probe とは独立した障害要因を混入させるため、アプリ自体の死活確認として不適切

正しい使い方は「Safe Probe は全て正常なのに CMS コンテンツが表示されない」という場面で、障害箇所(Vendure 側 / WordPress 側 / 両方)の特定を目的としてワンショットで呼び出すこと。

5.3 アプリケーションヘルスチェック実装例

// health-check.ts
import { Controller, Get } from "@nestjs/common";
import {
  HealthCheck,
  HealthCheckService,
  TypeOrmHealthIndicator,
  MemoryHealthIndicator,
  DiskHealthIndicator,
} from "@nestjs/terminus";

@Controller("health")
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private memory: MemoryHealthIndicator,
    private disk: DiskHealthIndicator,
    private redisHealth: RedisHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      // データベース接続チェック
      () => this.db.pingCheck("database"),

      // Redis 接続チェック
      () => this.redisHealth.pingCheck("redis"),

      // メモリ使用量チェック
      () => this.memory.checkHeap("memory_heap", 250 * 1024 * 1024),
      () => this.memory.checkRSS("memory_rss", 500 * 1024 * 1024),

      // ディスク使用量チェック
      () =>
        this.disk.checkStorage("storage", {
          path: "/",
          thresholdPercent: 0.85,
        }),

      // カスタムビジネスロジックチェック
      () => this.customBusinessHealthCheck(),
    ]);
  }

  @Get("readiness")
  @HealthCheck()
  readiness() {
    return this.health.check([
      () => this.db.pingCheck("database"),
      () => this.redisHealth.pingCheck("redis"),
    ]);
  }

  @Get("liveness")
  @HealthCheck()
  liveness() {
    return this.health.check([() => this.memory.checkHeap("memory_heap", 500 * 1024 * 1024)]);
  }

  private async customBusinessHealthCheck() {
    try {
      // 重要なビジネス機能の動作確認
      // 例: 商品検索機能、価格計算機能など

      const testProduct = await this.productService.findOne("test-product-id");
      if (!testProduct) {
        throw new Error("Test product not found");
      }

      const testPricing = await this.pricingService.calculatePrice(testProduct, "test-customer-id");
      if (!testPricing) {
        throw new Error("Pricing calculation failed");
      }

      return {
        "business-logic": {
          status: "up",
          details: {
            productSearch: "ok",
            pricingCalculation: "ok",
          },
        },
      };
    } catch (error) {
      return {
        "business-logic": {
          status: "down",
          details: {
            error: error.message,
          },
        },
      };
    }
  }
}

class RedisHealthIndicator {
  constructor(private redis: Redis) {}

  async pingCheck(key: string) {
    try {
      const result = await this.redis.ping();
      return {
        [key]: {
          status: result === "PONG" ? "up" : "down",
        },
      };
    } catch (error) {
      return {
        [key]: {
          status: "down",
          details: error.message,
        },
      };
    }
  }
}

6. ダッシュボード設定

6.1 Grafana ダッシュボード(テンプレート)

現時点で Grafana 自体は Fly.io の正式運用に含めない。以下は observability/grafana/ を再導入する場合のテンプレートとして保持する。

{
  "dashboard": {
    "title": "Vendure Production Dashboard",
    "panels": [
      {
        "title": "API Response Time",
        "type": "graph",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(vendure_api_duration_seconds_bucket[5m]))",
            "legendFormat": "95th percentile"
          },
          {
            "expr": "histogram_quantile(0.50, rate(vendure_api_duration_seconds_bucket[5m]))",
            "legendFormat": "50th percentile"
          }
        ],
        "yAxes": [
          {
            "label": "Response Time (seconds)",
            "max": 5
          }
        ]
      },
      {
        "title": "Request Rate",
        "type": "graph",
        "targets": [
          {
            "expr": "rate(vendure_api_requests_total[5m])",
            "legendFormat": "{{endpoint}} - {{method}}"
          }
        ]
      },
      {
        "title": "Error Rate",
        "type": "singlestat",
        "targets": [
          {
            "expr": "rate(vendure_api_requests_total{status=~\"5..\"}[5m]) / rate(vendure_api_requests_total[5m]) * 100"
          }
        ],
        "valueName": "current",
        "format": "percent",
        "thresholds": "1,5",
        "colorBackground": true
      },
      {
        "title": "Database Connections",
        "type": "graph",
        "targets": [
          {
            "expr": "postgres_active_connections",
            "legendFormat": "Active Connections"
          },
          {
            "expr": "postgres_connection_pool_size{state=\"idle\"}",
            "legendFormat": "Idle Pool"
          }
        ]
      },
      {
        "title": "Redis Memory Usage",
        "type": "graph",
        "targets": [
          {
            "expr": "redis_memory_usage_bytes / 1024 / 1024",
            "legendFormat": "Memory Usage (MB)"
          }
        ]
      },
      {
        "title": "Business Metrics",
        "type": "row",
        "panels": [
          {
            "title": "Orders per Hour",
            "type": "graph",
            "targets": [
              {
                "expr": "rate(vendure_orders_created_total[1h]) * 3600",
                "legendFormat": "{{customerType}}"
              }
            ]
          },
          {
            "title": "Revenue per Hour",
            "type": "graph",
            "targets": [
              {
                "expr": "rate(vendure_order_total_amount_sum[1h]) * 3600",
                "legendFormat": "Revenue (JPY/hour)"
              }
            ]
          }
        ]
      }
    ],
    "time": {
      "from": "now-6h",
      "to": "now"
    },
    "refresh": "30s"
  }
}

7. 運用プロセス

7.1 インシデント対応

// incident-response.ts
export class IncidentResponse {
  private static readonly SEVERITY_LEVELS = {
    P1: { name: "Critical", responseTime: 15, resolveTime: 4 * 60 }, // 15分以内対応、4時間以内解決
    P2: { name: "High", responseTime: 60, resolveTime: 24 * 60 }, // 1時間以内対応、24時間以内解決
    P3: { name: "Medium", responseTime: 4 * 60, resolveTime: 72 * 60 }, // 4時間以内対応、72時間以内解決
    P4: { name: "Low", responseTime: 24 * 60, resolveTime: 168 * 60 }, // 24時間以内対応、1週間以内解決
  };

  static async handleIncident(alert: Alert) {
    const severity = this.determineSeverity(alert);
    const incident = await this.createIncident(alert, severity);

    await this.notifyOnCall(incident);
    await this.executeRunbook(incident);

    return incident;
  }

  private static determineSeverity(alert: Alert): string {
    // アラートタイプに基づく重要度判定
    const criticalPatterns = [
      "application_down",
      "database_connection_failed",
      "order_failure_rate",
    ];

    if (criticalPatterns.some((pattern) => alert.title.includes(pattern))) {
      return "P1";
    }

    if (alert.severity === "critical") {
      return "P1";
    } else if (alert.severity === "warning") {
      return "P2";
    } else {
      return "P3";
    }
  }

  private static async executeRunbook(incident: Incident) {
    const runbook = this.getRunbook(incident.type);
    if (runbook) {
      await runbook.execute(incident);
    }
  }
}

// 自動復旧処理
export class AutoRecovery {
  static async attemptRecovery(alert: Alert): Promise<boolean> {
    switch (alert.title) {
      case "high_memory_usage":
        return await this.restartApplication();

      case "redis_connection_failed":
        return await this.reconnectRedis();

      case "database_connection_high":
        return await this.killIdleConnections();

      default:
        return false;
    }
  }

  private static async restartApplication(): Promise<boolean> {
    try {
      // Fly.io アプリケーション再起動
      execSync("flyctl machine restart", { timeout: 30000 });

      // ヘルスチェック待機
      await this.waitForHealthy(60000);

      return true;
    } catch (error) {
      console.error("Auto recovery failed:", error);
      return false;
    }
  }

  private static async waitForHealthy(timeout: number): Promise<void> {
    const start = Date.now();

    while (Date.now() - start < timeout) {
      try {
        const response = await fetch("/health/ready"); // `/health` は後方互換エイリアス。新規実装では `/health/ready` を使うこと
        if (response.ok) {
          return;
        }
      } catch {
        // 接続エラーは無視して継続
      }

      await new Promise((resolve) => setTimeout(resolve, 5000));
    }

    throw new Error("Health check timeout");
  }
}

7.2 定期メンテナンス

#!/bin/bash
# maintenance.sh - 定期メンテナンススクリプト

# データベースメンテナンス
postgres_maintenance() {
  echo "Running PostgreSQL maintenance..."

  flyctl postgres connect -a ritsubi-vendure-db << EOF
    VACUUM ANALYZE;
    REINDEX DATABASE ritsubi_vendure;

    -- 古いログの削除
    DELETE FROM vendure_session WHERE expires_at < NOW() - INTERVAL '7 days';
    DELETE FROM vendure_job_record WHERE finished_at < NOW() - INTERVAL '30 days';
EOF
}

# Redis メンテナンス
redis_maintenance() {
  echo "Running Redis maintenance..."

  # 期限切れキーの削除
  redis-cli --scan --pattern "expired:*" | xargs redis-cli del

  # メモリ最適化
  redis-cli MEMORY PURGE
}

# ログローテーション
log_rotation() {
  echo "Rotating application logs..."

  # 古いログファイルの圧縮・削除
  find /app/logs -name "*.log" -mtime +7 -exec gzip {} \;
  find /app/logs -name "*.log.gz" -mtime +30 -delete
}

# メトリクスクリーンアップ
metrics_cleanup() {
  echo "Cleaning up old metrics..."

  # 古いPrometheusメトリクスの削除
  # (通常はPrometheusサーバー側で設定)
}

# バックアップ検証
backup_verification() {
  echo "Verifying backups..."

  # 現行の正本は docs/05-delivery/maintenance/backup-and-restore.md
  gh run list --workflow backup-postgres-prd.yml --limit 12

  echo "Check latest objects under:"
  echo "  s3://ritsubi-ecommerce-backup/postgres/"
  echo "  s3://ritsubi-ecommerce-backup/wordpress/production/db/"
  echo "Expected cadence: hourly with 720h retention."
  echo "Alerts: BACKUP_NOTIFY_WEBHOOK_URL posts as Ritsubi Backup Alert with :warning:."
  echo "Fallback: Postgres failed R2 uploads may have postgres-backup-local-fallback artifacts; WordPress keeps /var/backups/ritsubi-wordpress/ for 72h."
  echo "Use AWS_REGION=auto / AWS_DEFAULT_REGION=auto for R2 verification."
}

# メイン実行
main() {
  echo "Starting maintenance at $(date)"

  postgres_maintenance
  redis_maintenance
  log_rotation
  metrics_cleanup
  backup_verification

  echo "Maintenance completed at $(date)"
}

main "$@"

文書バージョン: 1.0 作成日: 2025年9月17日 定期レビュー: 月次で監視設定とアラート閾値を見直し ��ー**: 月次で監視設定とアラート閾値を見直し ��