Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,12 @@ LOG_LEVEL=debug

# PWA設定
# アプリケーション名(マニフェストとメタデータに使用)
NEXT_PUBLIC_APP_NAME=Sonory
NEXT_PUBLIC_APP_NAME=Sonory

# オブザーバビリティ(APM)
# ブラウザ + Next.js: https://sentry.io でプロジェクト作成後に設定
NEXT_PUBLIC_SENTRY_DSN=
# Cloudflare Workers API: wrangler secret put SENTRY_DSN --env production
SENTRY_DSN=
# 本番以外で Sentry へ送る場合は true(通常は未設定で development はローカルのみ)
# NEXT_PUBLIC_SENTRY_ENABLED=false
8 changes: 4 additions & 4 deletions apps/api/src/middleware/monitoring.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Context, Next } from "hono"
import type { Env } from "../index"
import { logger } from "../utils/logger"
import { captureException } from "../utils/telemetry"

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

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

// TODO Phase 2以降: Sentryに送信
// if (c.env.SENTRY_DSN) {
// await sendToSentry(errorDetails)
// }
if (c.env.SENTRY_DSN) {
await captureException(error, errorDetails)
}

// エラーを再スロー(エラーハンドラーミドルウェアで処理)
throw error
Expand Down
2 changes: 2 additions & 0 deletions apps/api/src/types/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export interface Env {
SUPABASE_SERVICE_KEY?: string
PYTHON_AUDIO_ANALYZER_URL: string
PYTHON_AUDIO_ANALYZER_TIMEOUT: string
/** Sentry DSN(`wrangler secret put SENTRY_DSN` で設定) */
SENTRY_DSN?: string
}
31 changes: 31 additions & 0 deletions apps/api/src/utils/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { LogContext } from "@sonory/utils"

/**
* Workers 向けのエラー転送先。
*
* @description
* Phase 2: `@sentry/cloudflare` 導入後に実装を差し替える。
*/
export async function captureException(
error: unknown,
context?: LogContext,
): Promise<void> {
// Sentry DSN 設定後:
// import * as Sentry from "@sentry/cloudflare"
// Sentry.captureException(error, { extra: context })
void error
void context
}

/**
* 構造化メトリクスを Workers Analytics Engine へ送るためのプレースホルダー。
*/
export function recordMetric(
name: string,
value: number,
tags?: LogContext,
): void {
void name
void value
void tags
}
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@mapbox/mapbox-gl-geocoder": "^5.1.2",
"@opennextjs/cloudflare": "^1.19.11",
"@sonory/shared-types": "*",
"@sonory/utils": "*",
"@supabase/supabase-js": "^2.108.0",
"@tanstack/react-query": "^5.101.0",
"framer-motion": "^12.40.0",
Expand Down
50 changes: 50 additions & 0 deletions apps/web/src/lib/telemetry/createClientLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createLogger, type Logger } from "@sonory/utils"

import { createConsoleSink } from "./sinks/consoleSink"
import { createSentrySink } from "./sinks/sentrySink"

const isDevelopment = process.env.NODE_ENV === "development"

/**
* ブラウザ向けロガーを生成する。
*
* @description
* - `debug` / `info`: 開発環境のみコンソール出力
* - `warn` / `error`: 本番では Sentry へ転送(DSN 設定時)
* - 位置情報・音声 URL など PII は context に載せないこと
*
* @param namespace - ログの発生元(例: `"SoundPinMarkers"`)
* @returns 名前空間付きロガー
*
* @example
* ```ts
* const log = createClientLogger("recording")
* log.debug("Recorder started")
* log.error("Upload failed", { code: "NETWORK_ERROR" })
* ```
*/
export function createClientLogger(namespace: string): Logger {
const consoleSink = createConsoleSink()
const sentrySink = createSentrySink(namespace)

return createLogger({
namespace,
minLevel: isDevelopment ? "debug" : "warn",
sinks: {
debug: isDevelopment ? consoleSink.debug : undefined,
info: isDevelopment ? consoleSink.info : sentrySink.info,
warn: (message, context) => {
if (isDevelopment) {
consoleSink.warn(message, context)
}
sentrySink.warn(message, context)
},
error: (message, context) => {
if (isDevelopment) {
consoleSink.error(message, context)
}
sentrySink.error(message, context)
},
},
})
}
1 change: 1 addition & 0 deletions apps/web/src/lib/telemetry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { createClientLogger } from "./createClientLogger"
26 changes: 26 additions & 0 deletions apps/web/src/lib/telemetry/sinks/consoleSink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { LogContext, LogLevel } from "@sonory/utils"

/**
* 開発環境向けのコンソール出力シンク。
*/
export function createConsoleSink(): Readonly<
Record<LogLevel, (message: string, context?: LogContext) => void>
> {
const format = (message: string, context?: LogContext): string =>
context === undefined ? message : `${message} ${JSON.stringify(context)}`

return {
debug: (message, context) => {
console.debug(format(message, context))
},
info: (message, context) => {
console.info(format(message, context))
},
warn: (message, context) => {
console.warn(format(message, context))
},
error: (message, context) => {
console.error(format(message, context))
},
}
}
91 changes: 91 additions & 0 deletions apps/web/src/lib/telemetry/sinks/sentrySink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { LogContext } from "@sonory/utils"

/**
* Sentry SDK が提供する最小インターフェース。
*
* @description
* `@sentry/nextjs` 導入後に `window.Sentry` へバインドされる想定。
*/
interface SentryClient {
addBreadcrumb(breadcrumb: {
category: string
message: string
level?: "debug" | "info" | "warning" | "error"
data?: Record<string, unknown>
}): void
captureException(
error: unknown,
context?: { extra?: Record<string, unknown> },
): void
captureMessage(
message: string,
context?: {
level?: "warning" | "error"
extra?: Record<string, unknown>
},
): void
}

declare global {
interface Window {
Sentry?: SentryClient
}
}

function getSentryClient(): SentryClient | undefined {
if (typeof window === "undefined") {
return undefined
}

return window.Sentry
}

/**
* Sentry 向けのブレッドクラム・エラー転送シンク。
*
* @description
* DSN 未設定時は no-op。APM 導入後も呼び出し側の変更は不要。
*/
export function createSentrySink(namespace: string): {
info(message: string, context?: LogContext): void
warn(message: string, context?: LogContext): void
error(message: string, context?: LogContext): void
} {
const toBreadcrumb = (
level: "info" | "warning" | "error",
message: string,
context?: LogContext,
): void => {
const sentry = getSentryClient()
if (!sentry) {
return
}

sentry.addBreadcrumb({
category: namespace,
message,
level: level === "warning" ? "warning" : level,
data: context,
})
}

return {
info: (message, context) => {
toBreadcrumb("info", message, context)
},
warn: (message, context) => {
toBreadcrumb("warning", message, context)
getSentryClient()?.captureMessage(message, {
level: "warning",
extra: context,
})
},
error: (message, context) => {
toBreadcrumb("error", message, context)
getSentryClient()?.captureMessage(message, {
level: "error",
extra: context,
})
},
}
}
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export * from "./audio.js"

// 地理空間関連
export * from "./geo.js"
// テレメトリ関連
export * from "./telemetry/index.js"
// URL関連
export * from "./url.js"
export * from "./validation.js"
66 changes: 66 additions & 0 deletions packages/utils/src/telemetry/createLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { LogContext, LogLevel, Logger, LoggerConfig } from "./types.js"

const LOG_LEVEL_ORDER: Readonly<Record<LogLevel, number>> = {
debug: 0,
info: 1,
warn: 2,
error: 3,
}

/**
* 指定レベル以上のログのみを出力するロガーを生成する。
*
* @param config - 名前空間・最小レベル・出力先シンク
* @returns 構造化ログ用ロガー
*
* @example
* ```ts
* const logger = createLogger({
* namespace: "map",
* minLevel: "info",
* sinks: { info: (message) => console.info(message) },
* })
* logger.info("Map initialized", { zoom: 12 })
* ```
*/
export function createLogger(config: LoggerConfig): Logger {
const shouldLog = (level: LogLevel): boolean =>
LOG_LEVEL_ORDER[level] >= LOG_LEVEL_ORDER[config.minLevel]

const emit = (
level: LogLevel,
message: string,
context?: LogContext,
): void => {
if (!shouldLog(level)) {
return
}

const sink = config.sinks[level]
if (!sink) {
return
}

const payload =
context === undefined
? { namespace: config.namespace, message }
: { namespace: config.namespace, message, ...context }

sink(message, payload)
}

return {
debug: (message, context) => {
emit("debug", message, context)
},
info: (message, context) => {
emit("info", message, context)
},
warn: (message, context) => {
emit("warn", message, context)
},
error: (message, context) => {
emit("error", message, context)
},
}
}
8 changes: 8 additions & 0 deletions packages/utils/src/telemetry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export { createLogger } from "./createLogger.js"
export type {
LogContext,
LogLevel,
LogSink,
Logger,
LoggerConfig,
} from "./types.js"
41 changes: 41 additions & 0 deletions packages/utils/src/telemetry/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* テレメトリで扱うログレベル。
*/
export type LogLevel = "debug" | "info" | "warn" | "error"

/**
* 構造化ログに付与する追加コンテキスト。
*/
export type LogContext = Readonly<Record<string, unknown>>

/**
* ログを外部サービスへ転送するためのシンク。
*
* @description
* APM(Sentry 等)や開発用コンソール出力を差し替え可能にする。
*/
export interface LogSink {
readonly debug?: (message: string, context?: LogContext) => void
readonly info?: (message: string, context?: LogContext) => void
readonly warn?: (message: string, context?: LogContext) => void
readonly error?: (message: string, context?: LogContext) => void
}

/**
* ロガー生成時の設定。
*/
export interface LoggerConfig {
readonly namespace: string
readonly minLevel: LogLevel
readonly sinks: LogSink
}

/**
* アプリケーション全体で利用するロガーインターフェース。
*/
export interface Logger {
debug(message: string, context?: LogContext): void
info(message: string, context?: LogContext): void
warn(message: string, context?: LogContext): void
error(message: string, context?: LogContext): void
}
Loading