Skip to content

Commit 36b7a73

Browse files
committed
feat: add telemetry foundation for APM integration
Add shared createLogger utility and client-side telemetry sinks so debug console.log can be replaced with environment-aware logging. Sentry hooks are stubbed for web (window.Sentry) and API (SENTRY_DSN) until DSN is configured. Document observability env vars in .env.example.
1 parent b09ceb4 commit 36b7a73

14 files changed

Lines changed: 333 additions & 5 deletions

File tree

.env.example

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,12 @@ LOG_LEVEL=debug
3131

3232
# PWA設定
3333
# アプリケーション名(マニフェストとメタデータに使用)
34-
NEXT_PUBLIC_APP_NAME=Sonory
34+
NEXT_PUBLIC_APP_NAME=Sonory
35+
36+
# オブザーバビリティ(APM)
37+
# ブラウザ + Next.js: https://sentry.io でプロジェクト作成後に設定
38+
NEXT_PUBLIC_SENTRY_DSN=
39+
# Cloudflare Workers API: wrangler secret put SENTRY_DSN --env production
40+
SENTRY_DSN=
41+
# 本番以外で Sentry へ送る場合は true(通常は未設定で development はローカルのみ)
42+
# NEXT_PUBLIC_SENTRY_ENABLED=false

apps/api/src/middleware/monitoring.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Context, Next } from "hono"
22
import type { Env } from "../index"
33
import { logger } from "../utils/logger"
4+
import { captureException } from "../utils/telemetry"
45

56
/**
67
* モニタリング・ログ基盤ミドルウェア
@@ -38,10 +39,9 @@ export async function errorTracking(
3839

3940
logger.error("Unhandled error occurred", errorDetails)
4041

41-
// TODO Phase 2以降: Sentryに送信
42-
// if (c.env.SENTRY_DSN) {
43-
// await sendToSentry(errorDetails)
44-
// }
42+
if (c.env.SENTRY_DSN) {
43+
await captureException(error, errorDetails)
44+
}
4545

4646
// エラーを再スロー(エラーハンドラーミドルウェアで処理)
4747
throw error

apps/api/src/types/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ export interface Env {
99
SUPABASE_SERVICE_KEY?: string
1010
PYTHON_AUDIO_ANALYZER_URL: string
1111
PYTHON_AUDIO_ANALYZER_TIMEOUT: string
12+
/** Sentry DSN(`wrangler secret put SENTRY_DSN` で設定) */
13+
SENTRY_DSN?: string
1214
}

apps/api/src/utils/telemetry.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { LogContext } from "@sonory/utils"
2+
3+
/**
4+
* Workers 向けのエラー転送先。
5+
*
6+
* @description
7+
* Phase 2: `@sentry/cloudflare` 導入後に実装を差し替える。
8+
*/
9+
export async function captureException(
10+
error: unknown,
11+
context?: LogContext,
12+
): Promise<void> {
13+
// Sentry DSN 設定後:
14+
// import * as Sentry from "@sentry/cloudflare"
15+
// Sentry.captureException(error, { extra: context })
16+
void error
17+
void context
18+
}
19+
20+
/**
21+
* 構造化メトリクスを Workers Analytics Engine へ送るためのプレースホルダー。
22+
*/
23+
export function recordMetric(
24+
name: string,
25+
value: number,
26+
tags?: LogContext,
27+
): void {
28+
void name
29+
void value
30+
void tags
31+
}

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"@mapbox/mapbox-gl-geocoder": "^5.1.2",
3030
"@opennextjs/cloudflare": "^1.19.11",
3131
"@sonory/shared-types": "*",
32+
"@sonory/utils": "*",
3233
"@supabase/supabase-js": "^2.108.0",
3334
"@tanstack/react-query": "^5.101.0",
3435
"framer-motion": "^12.40.0",
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createLogger, type Logger } from "@sonory/utils"
2+
3+
import { createConsoleSink } from "./sinks/consoleSink"
4+
import { createSentrySink } from "./sinks/sentrySink"
5+
6+
const isDevelopment = process.env.NODE_ENV === "development"
7+
8+
/**
9+
* ブラウザ向けロガーを生成する。
10+
*
11+
* @description
12+
* - `debug` / `info`: 開発環境のみコンソール出力
13+
* - `warn` / `error`: 本番では Sentry へ転送(DSN 設定時)
14+
* - 位置情報・音声 URL など PII は context に載せないこと
15+
*
16+
* @param namespace - ログの発生元(例: `"SoundPinMarkers"`)
17+
* @returns 名前空間付きロガー
18+
*
19+
* @example
20+
* ```ts
21+
* const log = createClientLogger("recording")
22+
* log.debug("Recorder started")
23+
* log.error("Upload failed", { code: "NETWORK_ERROR" })
24+
* ```
25+
*/
26+
export function createClientLogger(namespace: string): Logger {
27+
const consoleSink = createConsoleSink()
28+
const sentrySink = createSentrySink(namespace)
29+
30+
return createLogger({
31+
namespace,
32+
minLevel: isDevelopment ? "debug" : "warn",
33+
sinks: {
34+
debug: isDevelopment ? consoleSink.debug : undefined,
35+
info: isDevelopment ? consoleSink.info : sentrySink.info,
36+
warn: (message, context) => {
37+
if (isDevelopment) {
38+
consoleSink.warn(message, context)
39+
}
40+
sentrySink.warn(message, context)
41+
},
42+
error: (message, context) => {
43+
if (isDevelopment) {
44+
consoleSink.error(message, context)
45+
}
46+
sentrySink.error(message, context)
47+
},
48+
},
49+
})
50+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { createClientLogger } from "./createClientLogger"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { LogContext, LogLevel } from "@sonory/utils"
2+
3+
/**
4+
* 開発環境向けのコンソール出力シンク。
5+
*/
6+
export function createConsoleSink(): Readonly<
7+
Record<LogLevel, (message: string, context?: LogContext) => void>
8+
> {
9+
const format = (message: string, context?: LogContext): string =>
10+
context === undefined ? message : `${message} ${JSON.stringify(context)}`
11+
12+
return {
13+
debug: (message, context) => {
14+
console.debug(format(message, context))
15+
},
16+
info: (message, context) => {
17+
console.info(format(message, context))
18+
},
19+
warn: (message, context) => {
20+
console.warn(format(message, context))
21+
},
22+
error: (message, context) => {
23+
console.error(format(message, context))
24+
},
25+
}
26+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import type { LogContext } from "@sonory/utils"
2+
3+
/**
4+
* Sentry SDK が提供する最小インターフェース。
5+
*
6+
* @description
7+
* `@sentry/nextjs` 導入後に `window.Sentry` へバインドされる想定。
8+
*/
9+
interface SentryClient {
10+
addBreadcrumb(breadcrumb: {
11+
category: string
12+
message: string
13+
level?: "debug" | "info" | "warning" | "error"
14+
data?: Record<string, unknown>
15+
}): void
16+
captureException(
17+
error: unknown,
18+
context?: { extra?: Record<string, unknown> },
19+
): void
20+
captureMessage(
21+
message: string,
22+
context?: {
23+
level?: "warning" | "error"
24+
extra?: Record<string, unknown>
25+
},
26+
): void
27+
}
28+
29+
declare global {
30+
interface Window {
31+
Sentry?: SentryClient
32+
}
33+
}
34+
35+
function getSentryClient(): SentryClient | undefined {
36+
if (typeof window === "undefined") {
37+
return undefined
38+
}
39+
40+
return window.Sentry
41+
}
42+
43+
/**
44+
* Sentry 向けのブレッドクラム・エラー転送シンク。
45+
*
46+
* @description
47+
* DSN 未設定時は no-op。APM 導入後も呼び出し側の変更は不要。
48+
*/
49+
export function createSentrySink(namespace: string): {
50+
info(message: string, context?: LogContext): void
51+
warn(message: string, context?: LogContext): void
52+
error(message: string, context?: LogContext): void
53+
} {
54+
const toBreadcrumb = (
55+
level: "info" | "warning" | "error",
56+
message: string,
57+
context?: LogContext,
58+
): void => {
59+
const sentry = getSentryClient()
60+
if (!sentry) {
61+
return
62+
}
63+
64+
sentry.addBreadcrumb({
65+
category: namespace,
66+
message,
67+
level: level === "warning" ? "warning" : level,
68+
data: context,
69+
})
70+
}
71+
72+
return {
73+
info: (message, context) => {
74+
toBreadcrumb("info", message, context)
75+
},
76+
warn: (message, context) => {
77+
toBreadcrumb("warning", message, context)
78+
getSentryClient()?.captureMessage(message, {
79+
level: "warning",
80+
extra: context,
81+
})
82+
},
83+
error: (message, context) => {
84+
toBreadcrumb("error", message, context)
85+
getSentryClient()?.captureMessage(message, {
86+
level: "error",
87+
extra: context,
88+
})
89+
},
90+
}
91+
}

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)