Observability
状态: 2026-06-06 基于代码 verify 定位: 工程实操 / 字段级约束。架构层"observability 是横切层"已在 architecture/2-backend.md 隐含,本文是接入细节。 不重复: 各端 SDK 升级 / 版本 cutover 见各 repo CLAUDE.md。本文只讲"现状怎么 wired"。
三端速查
跨端串联走 tracePropagationTargets(前端) → X-Request-Id header(apps/api) → SQS message attribute rootCorrelationId(backend Lambdas)。
1. Frontend (apps/web)
studio-website-monorepo/apps/web Vue 3 SPA,部署在 Cloudflare Pages。
初始化时机:src/main.ts:61 initializeSentry(app, router) + src/main.ts:64 setupSessionTracking(),在 bootstrap() 里 setupPlugins / app.mount 之前。
环境 gate:src/utils/sentry.ts:24 判 hostname === 'localhost' || '127.0.0.1' → shouldEnable = false,localhost 完全不 init(包括不发请求);否则取 stage 作 environment(test / pre / prod)。
上报内容(src/utils/sentry.ts:99-119):
browserTracingIntegration({ router })— Vue Router 导航追踪replayIntegration— 5% 采样 + 错误时 100%(prod);maskAllText: true+blockAllMedia: true全脱敏feedbackIntegration— 用户反馈表单,error 时显式触发(autoInject: false)tracesSampleRate: 0.1(prod) /1.0(test/pre)
分布式 trace:src/utils/sentry.ts:96 tracePropagationTargets: ['localhost', /^https:\/\/.*\.retaintive\.ai/] — 给 retaintive 域名加 sentry-trace / baggage header,串到 apps/api 的 Sentry trace。
User context:
- 登录后调
src/utils/sentry.ts:37setSentryUser(cognitoSub, email, username)—id用 Cognito sub(永久 ID) - 登出调
src/utils/sentry.ts:52clearSentryUser()→Sentry.setUser(null) - 自建 session analytics(
src/utils/sentry-analytics.ts) 还会另起sessionId、track 页面访问 / feature 使用 / org 切换;通过Sentry.setContext('session', ...)和Sentry.setTag('organization.current', orgId)附在每个 event 上(sentry-analytics.ts:90-95, 191)。
session analytics 接口(src/utils/sentry-analytics.ts:11-21, 26-35):
SessionAnalytics— sessionId / startTime / pagesVisited / totalActions / featuresUsed / organizations / errorsFeatureCategoryenum — DASHBOARD / CALL_ANALYSIS / FILTERS / EXPORTS / AUDIO / SEARCH / SETTINGS / ORGANIZATION- composable
src/composables/analytics/useSentryAnalytics.ts暴露trackAction / trackFilter / trackExport / trackSearch / trackDateRange / trackOrgSwitch / trackCallView / trackAudioPlayback / trackTableAction / trackCacheOperation / trackModal / trackSnapshot / trackThemeChange,全部通过Sentry.addBreadcrumb+setContext('user_actions', ...)上报。analytics 错误用 try-catch 吞掉,不影响 app(useSentryAnalytics.ts:122-128)。
噪音过滤(src/utils/sentry.ts:125-189):
ignoreErrors:ResizeObserver、浏览器扩展、Script error.— 永远 dropbeforeSend:网络错误 / chunk 加载失败默认 drop;但 ErrorBoundary 捕获的会 bypass(event.tags?.error_boundary→ return event),保证 UI 崩溃必上报beforeSend也负责脱敏 — 删Authorizationheader,URL 里token=替换为[REDACTED]
Release 版本:release: import.meta.env.VITE_CF_PAGES_COMMIT_SHA || 'studio-web@<stage>'(src/utils/sentry.ts:82),CF Pages 自动注入 commit SHA。
调试:
- 本地 localhost 直接跳过 init,改 hostname 才能验上报(或临时把
shouldEnablehardcodetrue) - Sentry 项目:
studio-weborgo4510203520745472(DSN 在sentry.ts:75,public 安全) - 控制台会打
Sentry initialized for environment: <env>(sentry.ts:192)
2. apps/api Lambda
studio-website-monorepo/apps/api Hono on AWS Lambda。
Cold start 接入(src/lambda.ts:13):initSentry() 在模块顶层调用 — 一次 cold start 跑一次。initSentry() 内部读 process.env['SENTRY_DSN'],没设就 warn 并 skip(services/observability.ts:51-72)。
handler 包装(src/lambda.ts:26):
Sentry.wrapHandler 加 cold start tag、timeout warning(~500ms 前触发)、自动 flush。callbackWaitsForEmptyEventLoop = false 由 wrapHandler 设,所以 flushSentryMetrics() 必须 await — 否则 just-metrics 的 HTTP 请求会被 Lambda freeze 砍掉(services/observability.ts:74-84 注释解释)。
双 metrics 通道:同一份 metric 同时写 CloudWatch EMF(免费,通过 Powertools Metrics)和 Sentry metrics(通过 just-metrics,同 DSN)。两边 dashboard 各自看。
Metric 命名 convention:
- Powertools 用 PascalCase:
EndpointLatency/ClientError/ServerError/CacheHit/CacheMiss/RingCentralLatency/RingCentralError/TokenRefreshSuccess/TokenRefreshFailure/OAuthSuccess/OAuthFailure/DynamoDBLatency/DynamoDBError/DynamoDBConsumedCapacity/CallsQueried/StoreOperation/PhoneNumbersSynced/ConnectionStateChange/SharedAccessOperation/WebhookEventReceived/RecordingDownloadSuccess/RecordingDownloadFailure/RecordingSize(services/observability.ts:99-314) - Sentry 用 dot.lowercase:
api.latency/api.client_error/api.server_error/cache.hit/cache.miss/ringcentral.latency/ringcentral.error/token.refresh_success/oauth.success等 - Namespace
Studio,默认 dimensionenvironment(services/observability.ts:36-42)
Path normalization(services/observability.ts:137-161):UUID → /:id、s-xxxx session id → /:sessionId、纯数字 → /:id、非 /v2/ 开头 → /_other。未 normalize 的旧版每个 unique path 一个 metric,导致 619 unique paths → 1570 metrics → ~$19/月(代码注释引 AWS Cost Explorer 数据)。
Structured log(middleware/tracing.ts):每 request crypto.randomUUID() 或读 X-Request-Id header → c.set('requestId') + c.header('X-Request-Id') + logger.appendKeys({ requestId })。所有后续 logger.info / logger.error 自动带 requestId。响应 JSON 也会被 inject requestId 字段(tracing.ts:75-90)。
X-Ray subsegment(tracing.ts:44-53):每请求 addNewSubsegment('hono-request'),annotation 加 requestId / path / method。tracer.enabled = false when config.environment === 'test'(services/observability.ts:30-33)。
Error 上报路径(middleware/error-handler.ts):
AppError(已知业务错误)→logger.warn+recordClientError/recordServerError,不 capture Sentry- 其他未捕获错误 →
logger.error+captureError(err, { requestId, path, method })(Sentry)+recordError('Unhandled')+recordServerError('INTERNAL_ERROR')(error-handler.ts:40-53)
tracesSampleRate:prod 0.1 / 非 prod 1.0(services/observability.ts:63)。
调试:
- CloudWatch Logs Insights 按
requestId串整条请求:fields @timestamp, @message | filter requestId = "<uuid>" - 慢 endpoint 看 CloudWatch metric
Studionamespace →EndpointLatency,按path+methoddimension 分组 - 同样 metric 在 Sentry
api.latencydistribution,按 attributepathfilter - 没设
SENTRY_DSN:logger.warn 打SENTRY_DSN not set, Sentry disabled,sentryMetrics 整体 no-op
3. Backend Lambda (callytics-infrastructure)
callytics-infrastructure/lambda/* — pipeline Lambda(ai-analysis-processor / message-processor / transcribe-processor / contacts-analyzer / neon-sync / reconciliation-orchestrator / storeid-coverage-monitor / alarm-dispatcher / config-manager 等)。
核心 pattern:createLogger(name) + wrapHandler(rawHandler)。
fan-out 设计(lambda/shared/utils/logger.ts:284-321):logger.error(message, data) 一次调用,自动:
powertoolsLogger.error()写 CloudWatch(structured JSON,自动带serviceName/environment/ requestId / messageId / clientId / telephonySessionId / rootCorrelationId / phone / storeId / source)notifyLarkError(error, context)fire-and-forget 推 Lark webhook(经lark-dm-botworker dedup 或直推,见lambda/shared/utils/lark.ts)captureException(error, { lambdaName, ...loggerContext, ...data })Sentry capture
⚠️ 代码注释里写 "Discord + Lark + Sentry" 是历史残留 — 当前仓库没有 Discord webhook 实际 wiring(grep 全
lambda/无DISCORD_WEBHOOK或discord.com/api/webhooks),fan-out 实际只有 Lark + Sentry(CloudWatch 是 Powertools 默认写入,不算 fan-out target)。
Sentry 初始化(lambda/shared/sentry.ts:25-62):
initSentry(serviceName)模块顶层调一次,内部用initializedflag 防重复- 没
SENTRY_DSN就 warn 一次,所有 helper(captureException/setSentryUser/addBreadcrumb/flush)变 no-op release: callytics-<serviceName>@<env>,tracesSampleRate: prod 0.1 / 其他 1.0,sendDefaultPii: false(后端不带 PII,跟 frontend 不同)beforeSend跟 frontend 一样脱敏token=和Authorizationheader(sentry.ts:49-58)createLogger(serviceName)会自动调initSentry(serviceName)(logger.ts:243)— 用createLogger的 Lambda 不需要单独initSentry,但显式调更清晰
wrapHandler pattern(各 Lambda handler 末尾):
没 SENTRY_DSN 时 wrapHandler 直接返回原 handler(sentry.ts:71-76),不付包装成本。
跨 Lambda trace 串联 — 不是靠 Sentry tracing(后端 Lambda 间 SQS 不自动 propagate),而是靠 lambda/shared/utils/correlation.ts:
rootCorrelationId从 SQS message attribute 取(correlation.ts:41),fallback 用sqsRecord.messageIdsetLoggerContext({ rootCorrelationId, phone, storeId, source, ... })把它 append 到 Powertools logger 的 persistent keys(logger.ts:71-74),整条 invocation 的日志都带这个 ID- CloudWatch Logs Insights 按
rootCorrelationIdfilter 可以串起一条 RC webhook → AI analysis → message → contacts 全 4 阶段的日志
重要 hygiene(logger.ts:80-101):invocation 结束必须 clearLoggerContext() 移除 persistent keys,否则 warm container 下一条 SQS record 会继承上一条的 rootCorrelationId / clientId(Codex review 2026-05-03 catch)。
周期 Lambda 监控模板:storeid-coverage-monitor(lambda/storeid-coverage-monitor/src/handler.ts)是 "EventBridge cron + DB 查询 + logger.error 越阈触发 fan-out" 的模板。设计 rationale(handler.ts:9-16):
- 用
logger.error直接走 Lark + Sentry,$0/月 vs CloudWatch alarm ~$3.70/月 — 选这条 pattern 是因为 storeid 监控是 "跨 6 表派生指标",DB 查询型不适合 CloudWatch metric 表达 - 原
handler.ts:9-16注释也提到 "SNS topic 在 test/pre 没人订阅,alarm 黑洞" — 该顾虑在 PR #1049 之后已不成立(AlarmDispatcherLambda 已订阅 SNS topic);现在 CloudWatch alarm 与周期 Lambda 是两条 first-class pattern,按"监控的对象"选(metric 越阈 vs 跨表业务不变式),不再因 SNS 黑洞回避 alarm 路径
SNS alarm dispatcher(PR #737 / #1049 补的修复路径,lib/stacks/monitoring-stack.ts:216-251):
- CloudWatch alarm 仍然
SnsAction到alarmTopic - 但新加了一个
AlarmDispatcherLambda 订阅这个 SNS topic,handler 把 alarm payload 转成logger.error()→ 走 Lark + Sentry 通道 - 解决"alarm 有但没人订阅"问题(issue #723)
- 所有环境无条件创建,SENTRY_DSN 显式写在 environment(
monitoring-stack.ts:243)
SQS DLQ(lib/stacks/sqs-stack.ts):每个主队列配 dedicated DLQ,14 天 retention(callLogDLQ / transcriptionResultDLQ / reconciliationDLQ / dailyBatchDLQ / leadProcessorDLQ)。CloudWatch alarm 监控 DLQ 消息数,触发后走 alarm-dispatcher 路径。
调试:
- CloudWatch Logs Insights filter
rootCorrelationId = "..."串整条 pipeline - Lark "View Logs" deep link 用
AWS_LAMBDA_FUNCTION_NAME(runtime auto-inject)构造正确 log group path(logger.ts:234-240,issue #738) - Sentry 项目用单一 org,各 Lambda 通过
release: callytics-<serviceName>@<env>区分 - 想看一个 catch block 的完整错误链(Node 20+ fetch 错误的真正 root cause 在
error.cause里),用extractErrorDetails(err)(logger.ts:172-202)展开errorType/errorCause(递归 3 层)/errorStack(前 5 帧)
4. Alert Routing — 哪种错走哪条通道
决策树:
- 后端 Lambda 里写代码 → 想触发 Lark 通知 → 走
logger.error(不要直接调notifyLarkError,会丢 CloudWatch + Sentry) - 后端 Lambda 想抓 unhandled exception →
wrapHandler已经管,Sentry 自动捕,但 Lark 不会响(因为没经过 logger.error)— 想响就 catch +logger.error重抛 - apps/api 想抓 unhandled → 已经有
errorHandlermiddleware,自动 Sentry + metric - 前端想加追踪事件 →
useSentryAnalytics()composable,不要直接Sentry.captureMessage
5. Common Tasks
找一个 user 的前端错误:
- Sentry → studio-web project → filter
user.id:<cognitoSub>或user.email:<email>(sentry-analytics.ts:77-82设的) - 同一 user 跨 session 用
cognitoSub,单 session 用session.id(setContext设的)
debug apps/api endpoint 慢:
- CloudWatch metric
Studionamespace →EndpointLatency→ 按path+methoddimension 看 p50/p99 - 或 Sentry →
api.latencydistribution,按 attribute filter - 进 CloudWatch Logs Insights:
fields @timestamp, requestId, duration | filter path = "/v2/calls" | sort duration desc | limit 20,拿 requestId 再fields @message | filter requestId = "<id>"看整条请求
监控新 backend metric(两条 first-class pattern,按"监控的对象"选):
- 某 CloudWatch metric 越阈(Lambda Duration / DLQ depth / Queue age / EMF custom)→ 用
CloudWatch alarm + SnsAction → alarmTopic,monitoring-stack 的alarm-dispatcherLambda 已订阅该 SNS topic,自动路由到 Lark + Sentry。模板看lib/stacks/monitoring-stack.ts已有 12 个 DLQ / Errors / Duration alarm。 - 跨表 / 跨 service 业务不变式(如 storeId NULL 率 / 跨 6 表的派生指标)→ 写周期 Lambda,EventBridge cron 触发,异常时
logger.error(...)— 自动走 Lark + Sentry,无 SNS,$0/月。模板看lambda/storeid-coverage-monitor/src/handler.ts。 -
历史 caveat:PR #1049 之前 alarm topic 没 Lambda subscriber,CloudWatch alarm 是 "silent fire"(black hole 订阅者问题,#988)。现在已 wired,两条 pattern 都可用。验证方法见下条。
trigger 测试 alert:
- 后端:任何一个 Lambda 临时加
logger.error('test alert', { error: 'test' }),跑一次 → Lark 群里能看到 - 前端:本地 hostname 改成非 localhost(或临时 patch
shouldEnable = true)→ thrownew Error('test')→ Sentry 项目能看到
验证 CloudWatch alarm → SNS → Lark 整条链(改 monitoring-stack / 加新 alarm / on-call 季度演练 / 怀疑 silent failure 时跑):
用 aws cloudwatch set-alarm-state 强制 alarm 进 ALARM 状态 — 跳过等 metric 真值跨阈值,直接走 alarm action(SNS publish),整条 chain 跟真实 fire 时无差(payload 一致,只是 StateReason 是手填的)。
历史:2026-05-31 起 7 天 silent failure(SNS topic policy 缺 Allow cloudwatch.amazonaws.com SNS:Publish,alarm 转 ALARM 但 SNS publish=0)→ PR #1049 fix → 2026-06-08 用上面方法 3 alarm × 2 卡片 = 6 张 Lark 卡片端到端 verified。详见 callytics-infrastructure#988。
串一条 pipeline 全链路日志:
- 从 apps/api 拿
X-Request-Idheader → 在 CloudWatch 找对应 Lambda 日志 - backend 阶段从一个 Lambda 拿到
rootCorrelationId(structured log 里)→ Logs Insights 跨 4 个 log group filter 同一 ID,看 RC webhook → AI analysis → message → contacts 完整时序
6. 不在本文范围
- 各 repo CLAUDE.md 里 Sentry 版本 / DSN 切换历史 / 自托管 → SaaS 迁移 timeline(动态,见各 repo)
storeid-coverage-monitor业务阈值调优、6 个表的 NULL 率定义(见 callytics-infrastructure handler.ts 顶部注释 + issue #647/#651)- alarm-dispatcher Lambda 内部 routing 逻辑(由 alarm
NewStateValue路由 error / info / warn,见 issue #723) - Lark webhook 鉴权 dedup 协议(
@retaintive/common/lark/notifier+retaintive/lark-dm-bot/service-errorworker,见 callytics-infrastructure.claude/specs/2026-05-15-unified-lark-alert-dedup-design.md) - 跨 repo trace 数据流 / Sentry 项目划分策略(retaintive 后续 review)