Skip to content

Commit d64d8e6

Browse files
committed
feat: integrate Sentry SDK for web and API
Add @sentry/nextjs with instrumentation hooks and global error boundary. Add @sentry/hono middleware for the Cloudflare Workers API. DSN is read only from environment variables so secrets stay out of the repository.
1 parent 36b7a73 commit d64d8e6

18 files changed

Lines changed: 1288 additions & 198 deletions

.env.example

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ LOG_LEVEL=debug
3333
# アプリケーション名(マニフェストとメタデータに使用)
3434
NEXT_PUBLIC_APP_NAME=Sonory
3535

36-
# オブザーバビリティ(APM)
37-
# ブラウザ + Next.js: https://sentry.io でプロジェクト作成後に設定
36+
# オブザーバビリティ(Sentry)
37+
# 1. https://sentry.io でプロジェクト作成(web: Next.js / api: Cloudflare Workers)
38+
# 2. 以下をローカル .env または Cloudflare / wrangler secret に設定(チャット等で共有しない)
3839
NEXT_PUBLIC_SENTRY_DSN=
39-
# Cloudflare Workers API: wrangler secret put SENTRY_DSN --env production
4040
SENTRY_DSN=
41-
# 本番以外で Sentry へ送る場合は true(通常は未設定で development はローカルのみ)
42-
# NEXT_PUBLIC_SENTRY_ENABLED=false
41+
# ソースマップ自動アップロード(任意・CI 向け)
42+
SENTRY_ORG=
43+
SENTRY_PROJECT=
44+
SENTRY_AUTH_TOKEN=

apps/api/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"dependencies": {
2828
"@hono/zod-openapi": "^1.4.0",
2929
"@hono/zod-validator": "^0.8.0",
30+
"@sentry/cloudflare": "^10.56.0",
31+
"@sentry/hono": "^10.56.0",
3032
"@sonory/shared-types": "file:../../packages/shared-types",
3133
"@sonory/utils": "file:../../packages/utils",
3234
"@supabase/supabase-js": "^2.108.0",

apps/api/src/app.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { OpenAPIHono } from "@hono/zod-openapi"
2+
import { sentry } from "@sentry/hono/cloudflare"
23
import { logger as honoLogger } from "hono/logger"
34
import { requestId } from "hono/request-id"
45
import { timing } from "hono/timing"
@@ -10,13 +11,19 @@ import { healthRoutes } from "./routes/health"
1011
import pinsRoutes from "./routes/pins"
1112
import type { Env } from "./types/env"
1213
import { logger } from "./utils/logger"
14+
import { createSentryOptions } from "./utils/sentryOptions"
1315

1416
/**
1517
* OpenAPIHono アプリケーションを構築する。
1618
*/
1719
export function createApp(): OpenAPIHono<{ Bindings: Env }> {
1820
const app = new OpenAPIHono<{ Bindings: Env }>()
1921

22+
app.use(
23+
"*",
24+
sentry(app, (env: Env) => createSentryOptions(env)),
25+
)
26+
2027
app.use("*", requestId())
2128
app.use("*", timing())
2229
app.use("*", honoLogger())

apps/api/src/middleware/error.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { APIError } from "@sonory/shared-types"
22
import { ERROR_CODES } from "@sonory/shared-types"
33
import type { Context, Next } from "hono"
44
import { HTTPException } from "hono/http-exception"
5+
import type { Env } from "../types/env"
6+
import { captureException } from "../utils/telemetry"
57

68
// ERROR_CODESを再エクスポート
79
export { ERROR_CODES }
@@ -85,6 +87,16 @@ export const errorHandler = async (c: Context, next: Next) => {
8587

8688
// その他のエラー
8789
console.error("Unhandled error:", error)
90+
91+
const env = (c.env ?? {}) as Env
92+
if (env.SENTRY_DSN) {
93+
await captureException(error, {
94+
requestId,
95+
path: c.req.path,
96+
method: c.req.method,
97+
})
98+
}
99+
88100
const apiError: APIError = {
89101
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
90102
message: "An unexpected error occurred",

apps/api/src/middleware/monitoring.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { captureException } from "../utils/telemetry"
99
* @description
1010
* Cloudflare Workers環境でのログ・メトリクス・エラー追跡を提供
1111
* Phase 1: 基本的なログ機能を実装
12-
* Phase 2以降: Sentry連携やCloudflare Analytics統合を追加予定
12+
* Phase 2: Sentry連携(@sentry/hono)済み
1313
*/
1414

1515
/**
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { CloudflareOptions } from "@sentry/cloudflare"
2+
import type { Env } from "../types/env"
3+
4+
/**
5+
* Workers 環境向け Sentry オプションを生成する。
6+
*/
7+
export function createSentryOptions(env: Env): CloudflareOptions {
8+
const isProduction = env.ENVIRONMENT === "production"
9+
10+
return {
11+
dsn: env.SENTRY_DSN,
12+
enabled: Boolean(env.SENTRY_DSN),
13+
environment: env.ENVIRONMENT,
14+
tracesSampleRate: isProduction ? 0.1 : 1,
15+
sendDefaultPii: false,
16+
}
17+
}

apps/api/src/utils/telemetry.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
1+
import * as Sentry from "@sentry/cloudflare"
12
import type { LogContext } from "@sonory/utils"
23

34
/**
4-
* Workers 向けのエラー転送先。
5-
*
6-
* @description
7-
* Phase 2: `@sentry/cloudflare` 導入後に実装を差し替える。
5+
* Workers 向けのエラー転送。
86
*/
97
export async function captureException(
108
error: unknown,
119
context?: LogContext,
1210
): Promise<void> {
13-
// Sentry DSN 設定後:
14-
// import * as Sentry from "@sentry/cloudflare"
15-
// Sentry.captureException(error, { extra: context })
16-
void error
17-
void context
11+
if (context === undefined) {
12+
Sentry.captureException(error)
13+
return
14+
}
15+
16+
Sentry.captureException(error, { extra: { ...context } })
1817
}
1918

2019
/**

apps/api/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"lib": ["ES2022"],
77
"types": ["./worker-configuration.d.ts", "node"],
88
"jsx": "react-jsx",
9-
"moduleResolution": "node",
9+
"moduleResolution": "bundler",
1010
"allowImportingTsExtensions": false,
1111
"noPropertyAccessFromIndexSignature": false,
1212
"moduleDetection": "force",

apps/web/instrumentation-client.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from "@sentry/nextjs"
2+
3+
import { getSentryOptions, isSentryEnabled } from "./sentry.shared.config"
4+
5+
if (isSentryEnabled()) {
6+
Sentry.init(getSentryOptions())
7+
}
8+
9+
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart

apps/web/instrumentation.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from "@sentry/nextjs"
2+
3+
/**
4+
* Next.js サーバー・Edge ランタイム向け Sentry 登録フック。
5+
*/
6+
export async function register(): Promise<void> {
7+
if (process.env.NEXT_RUNTIME === "nodejs") {
8+
await import("./sentry.server.config")
9+
}
10+
11+
if (process.env.NEXT_RUNTIME === "edge") {
12+
await import("./sentry.edge.config")
13+
}
14+
}
15+
16+
export const onRequestError = Sentry.captureRequestError

0 commit comments

Comments
 (0)