timsquad 3.3.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +288 -0
- package/README.md +158 -151
- package/dist/commands/compile.d.ts +3 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +170 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +94 -5
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/skills.d.ts +12 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +231 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/upgrade.js +5 -0
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/daemon/entry.js +3 -3
- package/dist/daemon/entry.js.map +1 -1
- package/dist/daemon/index.d.ts +3 -2
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +137 -45
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/meta-cache.d.ts +1 -0
- package/dist/daemon/meta-cache.d.ts.map +1 -1
- package/dist/daemon/meta-cache.js +9 -0
- package/dist/daemon/meta-cache.js.map +1 -1
- package/dist/daemon/session-state.d.ts +19 -0
- package/dist/daemon/session-state.d.ts.map +1 -0
- package/dist/daemon/session-state.js +132 -0
- package/dist/daemon/session-state.js.map +1 -0
- package/dist/daemon/shutdown.d.ts.map +1 -1
- package/dist/daemon/shutdown.js +7 -1
- package/dist/daemon/shutdown.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/compile-rules.d.ts +66 -0
- package/dist/lib/compile-rules.d.ts.map +1 -0
- package/dist/lib/compile-rules.js +114 -0
- package/dist/lib/compile-rules.js.map +1 -0
- package/dist/lib/compiler.d.ts +105 -0
- package/dist/lib/compiler.d.ts.map +1 -0
- package/dist/lib/compiler.js +368 -0
- package/dist/lib/compiler.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +8 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/template.d.ts.map +1 -1
- package/dist/lib/template.js +6 -0
- package/dist/lib/template.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +12 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/project.d.ts +1 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +2 -0
- package/dist/types/project.js.map +1 -1
- package/package.json +1 -1
- package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
- package/templates/base/skills/controller/SKILL.md +111 -0
- package/templates/base/skills/controller/references/README.md +35 -0
- package/templates/base/skills/controller/rules/README.md +18 -0
- package/templates/base/skills/mobile/dart/SKILL.md +69 -0
- package/templates/base/skills/mobile/dart/rules/async-patterns.md +112 -0
- package/templates/base/skills/mobile/dart/rules/code-style.md +96 -0
- package/templates/base/skills/mobile/dart/rules/null-safety.md +84 -0
- package/templates/base/skills/mobile/dart/rules/type-system.md +111 -0
- package/templates/base/skills/mobile/flutter/SKILL.md +89 -0
- package/templates/base/skills/mobile/flutter/ci-cd/SKILL.md +82 -0
- package/templates/base/skills/mobile/flutter/ci-cd/references/ci-cd-pipeline.md +314 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/code-signing.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/codemagic-setup.md +116 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/fastlane-setup.md +105 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/github-actions.md +112 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/store-deployment.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/versioning.md +107 -0
- package/templates/base/skills/mobile/flutter/i18n/SKILL.md +78 -0
- package/templates/base/skills/mobile/flutter/i18n/references/i18n-architecture.md +225 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/arb-files.md +182 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/locale-switching.md +226 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/localization-setup.md +137 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/plural-gender.md +159 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/text-direction.md +199 -0
- package/templates/base/skills/mobile/flutter/monitoring/SKILL.md +81 -0
- package/templates/base/skills/mobile/flutter/monitoring/references/monitoring-architecture.md +269 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/analytics.md +227 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/crashlytics-setup.md +195 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/logging.md +258 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/performance-monitoring.md +248 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/sentry-integration.md +249 -0
- package/templates/base/skills/mobile/flutter/networking/SKILL.md +88 -0
- package/templates/base/skills/mobile/flutter/networking/references/api-client-architecture.md +305 -0
- package/templates/base/skills/mobile/flutter/networking/rules/caching.md +212 -0
- package/templates/base/skills/mobile/flutter/networking/rules/connectivity.md +213 -0
- package/templates/base/skills/mobile/flutter/networking/rules/dio-setup.md +159 -0
- package/templates/base/skills/mobile/flutter/networking/rules/error-handling.md +209 -0
- package/templates/base/skills/mobile/flutter/networking/rules/interceptors.md +205 -0
- package/templates/base/skills/mobile/flutter/networking/rules/retrofit-patterns.md +194 -0
- package/templates/base/skills/mobile/flutter/push-notifications/SKILL.md +87 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/notification-architecture.md +340 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/platform-setup.md +286 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/background-processing.md +308 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/deep-linking.md +217 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/fcm-setup.md +164 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/local-notifications.md +262 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-handling.md +210 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-permissions.md +246 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/rich-notifications.md +320 -0
- package/templates/base/skills/mobile/flutter/references/freezed-patterns.md +162 -0
- package/templates/base/skills/mobile/flutter/references/project-structure.md +170 -0
- package/templates/base/skills/mobile/flutter/rules/animations.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/architecture.md +121 -0
- package/templates/base/skills/mobile/flutter/rules/navigation-routing.md +117 -0
- package/templates/base/skills/mobile/flutter/rules/performance.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/platform-adaptive.md +126 -0
- package/templates/base/skills/mobile/flutter/rules/state-management.md +110 -0
- package/templates/base/skills/mobile/flutter/rules/testing.md +131 -0
- package/templates/base/skills/mobile/flutter/rules/widget-conventions.md +122 -0
- package/templates/base/skills/mobile/flutter/security/SKILL.md +86 -0
- package/templates/base/skills/mobile/flutter/security/references/mobile-security-checklist.md +168 -0
- package/templates/base/skills/mobile/flutter/security/rules/api-key-protection.md +206 -0
- package/templates/base/skills/mobile/flutter/security/rules/authentication.md +248 -0
- package/templates/base/skills/mobile/flutter/security/rules/data-protection.md +271 -0
- package/templates/base/skills/mobile/flutter/security/rules/obfuscation.md +213 -0
- package/templates/base/skills/mobile/flutter/security/rules/secure-storage.md +171 -0
- package/templates/base/skills/mobile/flutter/security/rules/ssl-pinning.md +197 -0
- package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
- package/templates/platforms/claude-code/scripts/completion-guard.sh +57 -0
- package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
- package/templates/platforms/claude-code/settings.json +75 -3
- package/templates/project-types/mobile-app/config.yaml +123 -0
- package/templates/project-types/mobile-app/process/workflow.xml +191 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Monitoring Architecture & Dashboard Configuration
|
|
3
|
+
category: reference
|
|
4
|
+
source: internal
|
|
5
|
+
tags: architecture, dashboard, alerting, data-retention, crashlytics, sentry, analytics
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Monitoring Architecture & Dashboard Configuration
|
|
9
|
+
|
|
10
|
+
모니터링 레이어 구조, 대시보드 구성, 알림 설정, 데이터 보존 정책.
|
|
11
|
+
프로덕션 운영 시 참조.
|
|
12
|
+
|
|
13
|
+
## Key Concepts
|
|
14
|
+
|
|
15
|
+
- **레이어 모델**: crash → error → performance → analytics → logging (우선순위 순)
|
|
16
|
+
- **대시보드 분리**: Firebase Console (크래시/성능) + Sentry (에러 컨텍스트)
|
|
17
|
+
- **알림 전략**: 크래시 스파이크, ANR, 성능 저하에 대한 자동 알림
|
|
18
|
+
- **데이터 보존**: 도구별 보존 기간 + 비용 최적화
|
|
19
|
+
|
|
20
|
+
## Monitoring Layer Model
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────────────────────────────────────────────┐
|
|
24
|
+
│ Monitoring Layers │
|
|
25
|
+
├─────────┬───────────────┬───────────────┬───────────────┤
|
|
26
|
+
│ Layer │ Tool │ Priority │ Purpose │
|
|
27
|
+
├─────────┼───────────────┼───────────────┼───────────────┤
|
|
28
|
+
│ L1 │ Crashlytics │ CRITICAL │ 앱 크래시 │
|
|
29
|
+
│ Crash │ │ │ ANR/프리징 │
|
|
30
|
+
│ │ │ │ 안정성 % │
|
|
31
|
+
├─────────┼───────────────┼───────────────┼───────────────┤
|
|
32
|
+
│ L2 │ Sentry │ HIGH │ 비치명 에러 │
|
|
33
|
+
│ Error │ + Crashlytics │ │ 에러 컨텍스트 │
|
|
34
|
+
│ │ │ │ breadcrumbs │
|
|
35
|
+
├─────────┼───────────────┼───────────────┼───────────────┤
|
|
36
|
+
│ L3 │ Firebase │ HIGH │ 앱 시작 시간 │
|
|
37
|
+
│ Perf │ Performance │ │ HTTP 응답 │
|
|
38
|
+
│ │ │ │ 프레임 렌더링 │
|
|
39
|
+
├─────────┼───────────────┼───────────────┼───────────────┤
|
|
40
|
+
│ L4 │ Firebase │ MEDIUM │ 사용자 행동 │
|
|
41
|
+
│ Analyt │ Analytics │ │ 전환 퍼널 │
|
|
42
|
+
│ │ │ │ 리텐션 │
|
|
43
|
+
├─────────┼───────────────┼───────────────┼───────────────┤
|
|
44
|
+
│ L5 │ logger + │ LOW │ 디버깅 추적 │
|
|
45
|
+
│ Logging │ Sentry/Crash │ │ 원격 컨텍스트 │
|
|
46
|
+
│ │ │ │ 감사 로그 │
|
|
47
|
+
└─────────┴───────────────┴───────────────┴───────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Directory Structure
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
lib/
|
|
54
|
+
├── core/
|
|
55
|
+
│ └── monitoring/
|
|
56
|
+
│ ├── monitoring_service.dart # 통합 초기화 + Facade
|
|
57
|
+
│ ├── crashlytics/
|
|
58
|
+
│ │ ├── crashlytics_service.dart # Crashlytics 래퍼
|
|
59
|
+
│ │ └── crashlytics_error_handler.dart # 에러 핸들러 체인
|
|
60
|
+
│ ├── analytics/
|
|
61
|
+
│ │ ├── analytics_service.dart # Analytics 래퍼
|
|
62
|
+
│ │ ├── analytics_events.dart # 이벤트 상수
|
|
63
|
+
│ │ └── analytics_observer.dart # GoRouter observer
|
|
64
|
+
│ ├── performance/
|
|
65
|
+
│ │ ├── performance_service.dart # Custom trace 래퍼
|
|
66
|
+
│ │ └── performance_interceptor.dart # Dio HTTP metric
|
|
67
|
+
│ ├── sentry/
|
|
68
|
+
│ │ ├── sentry_service.dart # Sentry 래퍼
|
|
69
|
+
│ │ ├── sentry_breadcrumb_service.dart # Breadcrumb 관리
|
|
70
|
+
│ │ └── sentry_navigator_observer.dart # 네비게이션 추적
|
|
71
|
+
│ ├── logging/
|
|
72
|
+
│ │ ├── app_logger.dart # Logger 래퍼
|
|
73
|
+
│ │ ├── log_sanitizer.dart # 민감 데이터 마스킹
|
|
74
|
+
│ │ └── remote_log_output.dart # 원격 로그 출력
|
|
75
|
+
│ └── providers/
|
|
76
|
+
│ └── monitoring_providers.dart # Riverpod providers
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Initialization Order
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
main.dart 초기화 순서:
|
|
83
|
+
│
|
|
84
|
+
├─ 1. WidgetsFlutterBinding.ensureInitialized()
|
|
85
|
+
├─ 2. Firebase.initializeApp()
|
|
86
|
+
├─ 3. FlutterError.onError = Crashlytics.recordFlutterFatalError
|
|
87
|
+
├─ 4. PlatformDispatcher.instance.onError → Crashlytics.recordError
|
|
88
|
+
├─ 5. SentryFlutter.init(appRunner: ...)
|
|
89
|
+
│ └─ 내부에서 runApp() 호출
|
|
90
|
+
│ │
|
|
91
|
+
│ └─ App 초기화 시:
|
|
92
|
+
│ ├─ MonitoringService.initialize()
|
|
93
|
+
│ │ ├─ CrashlyticsService.setUser()
|
|
94
|
+
│ │ ├─ AnalyticsService.setUserProperties()
|
|
95
|
+
│ │ ├─ PerformanceService.enable()
|
|
96
|
+
│ │ └─ AppLogger.initialize()
|
|
97
|
+
│ └─ GoRouter observers: [AnalyticsObserver, SentryObserver]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Dashboard Configuration
|
|
101
|
+
|
|
102
|
+
### Firebase Console
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Firebase Console > Crashlytics:
|
|
106
|
+
├─ Overview: 크래시 없는 사용자 % (목표: 99.5% 이상)
|
|
107
|
+
├─ Issues: 크래시 목록 (발생 빈도, 영향 사용자 수)
|
|
108
|
+
├─ Trends: 버전별 안정성 추이
|
|
109
|
+
└─ Velocity alerts: 크래시 급증 알림
|
|
110
|
+
|
|
111
|
+
Firebase Console > Performance:
|
|
112
|
+
├─ Dashboard: 앱 시작 시간, 화면 렌더링
|
|
113
|
+
├─ Network: HTTP 요청 성능 (응답 시간, 성공률)
|
|
114
|
+
├─ Custom traces: 비즈니스 로직 성능
|
|
115
|
+
└─ Threshold alerts: 성능 임계값 알림
|
|
116
|
+
|
|
117
|
+
Firebase Console > Analytics:
|
|
118
|
+
├─ Dashboard: 활성 사용자, 세션
|
|
119
|
+
├─ Events: 이벤트 발생 빈도
|
|
120
|
+
├─ Funnels: 전환 퍼널 (가입 → 매치 생성 → 완료)
|
|
121
|
+
├─ Retention: 일별/주별 리텐션
|
|
122
|
+
└─ Audiences: 사용자 세그먼트
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Sentry Dashboard
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
Sentry > Issues:
|
|
129
|
+
├─ 에러 목록 (발생 빈도, 영향 사용자, 첫 발생/마지막 발생)
|
|
130
|
+
├─ 에러 상세 (breadcrumbs, 태그, 디바이스 정보)
|
|
131
|
+
└─ 에러 그룹화 (fingerprint 기반)
|
|
132
|
+
|
|
133
|
+
Sentry > Performance:
|
|
134
|
+
├─ 트랜잭션 목록 (p50, p75, p95)
|
|
135
|
+
├─ 느린 트랜잭션 식별
|
|
136
|
+
└─ Web Vitals (웹뷰 사용 시)
|
|
137
|
+
|
|
138
|
+
Sentry > Alerts:
|
|
139
|
+
├─ Issue alerts: 새 에러 발생 시
|
|
140
|
+
├─ Metric alerts: 에러율 임계값 초과 시
|
|
141
|
+
└─ Integrations: Slack, PagerDuty, Email
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Alerting Strategy
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
┌──────────────────┬────────────┬──────────────────────────────┐
|
|
148
|
+
│ Alert Type │ Severity │ Action │
|
|
149
|
+
├──────────────────┼────────────┼──────────────────────────────┤
|
|
150
|
+
│ Crash spike │ P0 │ Slack #alerts + PagerDuty │
|
|
151
|
+
│ (>1% 사용자 영향) │ │ 즉시 대응 (핫픽스 검토) │
|
|
152
|
+
├──────────────────┼────────────┼──────────────────────────────┤
|
|
153
|
+
│ ANR rate >0.5% │ P1 │ Slack #alerts │
|
|
154
|
+
│ │ │ 24시간 내 조사 │
|
|
155
|
+
├──────────────────┼────────────┼──────────────────────────────┤
|
|
156
|
+
│ New fatal crash │ P1 │ Slack #alerts │
|
|
157
|
+
│ │ │ 다음 스프린트 수정 │
|
|
158
|
+
├──────────────────┼────────────┼──────────────────────────────┤
|
|
159
|
+
│ Performance │ P2 │ Slack #monitoring │
|
|
160
|
+
│ degradation │ │ 앱 시작 >3s, API p95 >2s │
|
|
161
|
+
├──────────────────┼────────────┼──────────────────────────────┤
|
|
162
|
+
│ Error rate │ P2 │ Sentry alert │
|
|
163
|
+
│ spike (비치명) │ │ 다음 스프린트 우선순위 검토 │
|
|
164
|
+
├──────────────────┼────────────┼──────────────────────────────┤
|
|
165
|
+
│ Drop in daily │ P3 │ Weekly review │
|
|
166
|
+
│ active users │ │ Analytics 퍼널 분석 │
|
|
167
|
+
└──────────────────┴────────────┴──────────────────────────────┘
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Data Retention Policy
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
┌────────────────┬──────────────┬────────────┬─────────────────┐
|
|
174
|
+
│ Tool │ Free Tier │ Paid Tier │ Recommendation │
|
|
175
|
+
├────────────────┼──────────────┼────────────┼─────────────────┤
|
|
176
|
+
│ Crashlytics │ 90일 │ 90일 │ 그대로 사용 │
|
|
177
|
+
│ Analytics │ 14개월 │ 14개월 │ BigQuery 연동 │
|
|
178
|
+
│ Performance │ 자동 관리 │ 자동 관리 │ 그대로 사용 │
|
|
179
|
+
│ Sentry (Free) │ 30일 │ 90일 │ 주요 이슈 북마크 │
|
|
180
|
+
│ App logs │ 세션 내 │ — │ 원격 로깅 연동 │
|
|
181
|
+
└────────────────┴──────────────┴────────────┴─────────────────┘
|
|
182
|
+
|
|
183
|
+
BigQuery 연동 (Analytics 장기 보존):
|
|
184
|
+
Firebase Console > Project Settings > Integrations > BigQuery
|
|
185
|
+
- 일별 raw event export
|
|
186
|
+
- SQL 쿼리로 커스텀 분석
|
|
187
|
+
- Data Studio / Looker 대시보드 연동
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Monitoring Service (Facade)
|
|
191
|
+
|
|
192
|
+
```dart
|
|
193
|
+
/// 모니터링 통합 초기화 Facade
|
|
194
|
+
class MonitoringService {
|
|
195
|
+
final CrashlyticsService _crashlytics;
|
|
196
|
+
final AnalyticsService _analytics;
|
|
197
|
+
final PerformanceService _performance;
|
|
198
|
+
final SentryScopeManager _sentry;
|
|
199
|
+
final AppLogger _logger;
|
|
200
|
+
|
|
201
|
+
MonitoringService({
|
|
202
|
+
required CrashlyticsService crashlytics,
|
|
203
|
+
required AnalyticsService analytics,
|
|
204
|
+
required PerformanceService performance,
|
|
205
|
+
required SentryScopeManager sentry,
|
|
206
|
+
required AppLogger logger,
|
|
207
|
+
}) : _crashlytics = crashlytics,
|
|
208
|
+
_analytics = analytics,
|
|
209
|
+
_performance = performance,
|
|
210
|
+
_sentry = sentry,
|
|
211
|
+
_logger = logger;
|
|
212
|
+
|
|
213
|
+
/// 사용자 로그인 시 모든 서비스에 사용자 설정
|
|
214
|
+
Future<void> identifyUser({
|
|
215
|
+
required String userId,
|
|
216
|
+
String? email,
|
|
217
|
+
Map<String, String>? properties,
|
|
218
|
+
}) async {
|
|
219
|
+
await _crashlytics.setUser(userId);
|
|
220
|
+
await _analytics.setUserId(userId);
|
|
221
|
+
await _sentry.setUser(id: userId, email: email);
|
|
222
|
+
_logger.info('User identified: $userId');
|
|
223
|
+
|
|
224
|
+
if (properties != null) {
|
|
225
|
+
await _crashlytics.setContext(
|
|
226
|
+
appVersion: properties['app_version'] ?? '',
|
|
227
|
+
buildNumber: properties['build_number'] ?? '',
|
|
228
|
+
subscriptionTier: properties['tier'],
|
|
229
|
+
);
|
|
230
|
+
for (final entry in properties.entries) {
|
|
231
|
+
await _analytics.setUserProperty(
|
|
232
|
+
name: entry.key,
|
|
233
|
+
value: entry.value,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// 로그아웃 시 모든 서비스 정리
|
|
240
|
+
Future<void> clearUser() async {
|
|
241
|
+
await _crashlytics.clearUser();
|
|
242
|
+
await _analytics.setUserId(null);
|
|
243
|
+
await _sentry.clearUser();
|
|
244
|
+
_logger.info('User cleared from monitoring');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// Riverpod Provider
|
|
249
|
+
final monitoringServiceProvider = Provider<MonitoringService>((ref) {
|
|
250
|
+
return MonitoringService(
|
|
251
|
+
crashlytics: ref.watch(crashlyticsServiceProvider),
|
|
252
|
+
analytics: ref.watch(analyticsServiceProvider),
|
|
253
|
+
performance: ref.watch(performanceServiceProvider),
|
|
254
|
+
sentry: ref.watch(sentryScopeManagerProvider),
|
|
255
|
+
logger: AppLogger.instance,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Common Pitfalls
|
|
261
|
+
|
|
262
|
+
1. **초기화 순서**: Firebase → Crashlytics 핸들러 → Sentry → 나머지 (순서 중요)
|
|
263
|
+
2. **디버그 데이터 오염**: 개발 중 크래시/이벤트가 프로덕션 대시보드에 혼합 → 환경 분리
|
|
264
|
+
3. **이벤트 쿼터**: Sentry 무료 5K/월, Analytics 500이벤트/사용자/일 → 필터링 필수
|
|
265
|
+
4. **PII 유출**: 로그/이벤트에 이메일/전화번호 포함 → 마스킹 + sendDefaultPii:false
|
|
266
|
+
5. **소스맵 미업로드**: 난독화된 스택트레이스 → dSYM/ProGuard mapping 업로드 자동화
|
|
267
|
+
6. **과도한 breadcrumb**: Sentry 기본 100개 제한 → 중요한 것만 기록
|
|
268
|
+
7. **성능 오버헤드**: 모니터링 SDK 자체가 성능에 영향 → 샘플링률 조절
|
|
269
|
+
8. **알림 피로**: 모든 에러에 알림 → 임계값 기반 알림으로 전환
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Analytics Event Taxonomy & Screen Tracking
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: "이벤트 미설계 → 무의미한 데이터 축적, 의사결정 근거 부재"
|
|
5
|
+
tags: firebase-analytics, event, screen-view, user-property, observer
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Analytics Event Taxonomy & Screen Tracking
|
|
9
|
+
|
|
10
|
+
**Impact: HIGH (이벤트 미설계 → 무의미한 데이터 축적, 의사결정 근거 부재)**
|
|
11
|
+
|
|
12
|
+
Firebase Analytics 초기화, 이벤트 택소노미 설계, 사용자 속성,
|
|
13
|
+
AnalyticsObserver를 통한 화면 추적, 디버그 모드.
|
|
14
|
+
|
|
15
|
+
### 의존성
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# pubspec.yaml
|
|
19
|
+
dependencies:
|
|
20
|
+
firebase_core: ^3.8.0
|
|
21
|
+
firebase_analytics: ^11.4.0
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 이벤트 택소노미 설계
|
|
25
|
+
|
|
26
|
+
**Incorrect (무분별한 이벤트 로깅):**
|
|
27
|
+
```dart
|
|
28
|
+
// 이벤트 이름이 일관성 없고 파라미터 미정의
|
|
29
|
+
analytics.logEvent(name: 'clicked_button');
|
|
30
|
+
analytics.logEvent(name: 'user_did_something');
|
|
31
|
+
analytics.logEvent(name: 'pageView', parameters: {'p': 'home'});
|
|
32
|
+
// → 분석 불가능한 데이터 축적
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Correct (구조화된 택소노미):**
|
|
36
|
+
```dart
|
|
37
|
+
/// 이벤트 이름 상수 (snake_case, 40자 이하)
|
|
38
|
+
abstract class AnalyticsEvents {
|
|
39
|
+
// 화면 이벤트
|
|
40
|
+
static const screenView = 'screen_view';
|
|
41
|
+
|
|
42
|
+
// 사용자 액션
|
|
43
|
+
static const buttonClick = 'button_click';
|
|
44
|
+
static const featureUse = 'feature_use';
|
|
45
|
+
static const search = 'search';
|
|
46
|
+
|
|
47
|
+
// 비즈니스 이벤트
|
|
48
|
+
static const matchCreated = 'match_created';
|
|
49
|
+
static const matchJoined = 'match_joined';
|
|
50
|
+
static const matchCompleted = 'match_completed';
|
|
51
|
+
|
|
52
|
+
// 전환 이벤트
|
|
53
|
+
static const signUp = 'sign_up';
|
|
54
|
+
static const login = 'login';
|
|
55
|
+
static const subscriptionStarted = 'subscription_started';
|
|
56
|
+
|
|
57
|
+
// 에러 이벤트
|
|
58
|
+
static const errorOccurred = 'error_occurred';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// 파라미터 이름 상수
|
|
62
|
+
abstract class AnalyticsParams {
|
|
63
|
+
static const screenName = 'screen_name';
|
|
64
|
+
static const screenClass = 'screen_class';
|
|
65
|
+
static const buttonName = 'button_name';
|
|
66
|
+
static const featureName = 'feature_name';
|
|
67
|
+
static const sportType = 'sport_type';
|
|
68
|
+
static const errorType = 'error_type';
|
|
69
|
+
static const errorMessage = 'error_message';
|
|
70
|
+
static const source = 'source';
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Analytics 서비스
|
|
75
|
+
|
|
76
|
+
```dart
|
|
77
|
+
class AnalyticsService {
|
|
78
|
+
final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
|
|
79
|
+
|
|
80
|
+
/// 화면 조회
|
|
81
|
+
Future<void> logScreenView({
|
|
82
|
+
required String screenName,
|
|
83
|
+
String? screenClass,
|
|
84
|
+
}) async {
|
|
85
|
+
await _analytics.logScreenView(
|
|
86
|
+
screenName: screenName,
|
|
87
|
+
screenClass: screenClass ?? screenName,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// 버튼 클릭
|
|
92
|
+
Future<void> logButtonClick({
|
|
93
|
+
required String buttonName,
|
|
94
|
+
required String screenName,
|
|
95
|
+
}) async {
|
|
96
|
+
await _analytics.logEvent(
|
|
97
|
+
name: AnalyticsEvents.buttonClick,
|
|
98
|
+
parameters: {
|
|
99
|
+
AnalyticsParams.buttonName: buttonName,
|
|
100
|
+
AnalyticsParams.screenName: screenName,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// 기능 사용
|
|
106
|
+
Future<void> logFeatureUse({
|
|
107
|
+
required String featureName,
|
|
108
|
+
Map<String, Object>? extra,
|
|
109
|
+
}) async {
|
|
110
|
+
await _analytics.logEvent(
|
|
111
|
+
name: AnalyticsEvents.featureUse,
|
|
112
|
+
parameters: {
|
|
113
|
+
AnalyticsParams.featureName: featureName,
|
|
114
|
+
...?extra,
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// 비즈니스 이벤트
|
|
120
|
+
Future<void> logMatchCreated({
|
|
121
|
+
required String sportType,
|
|
122
|
+
required int maxPlayers,
|
|
123
|
+
}) async {
|
|
124
|
+
await _analytics.logEvent(
|
|
125
|
+
name: AnalyticsEvents.matchCreated,
|
|
126
|
+
parameters: {
|
|
127
|
+
AnalyticsParams.sportType: sportType,
|
|
128
|
+
'max_players': maxPlayers,
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Riverpod Provider
|
|
135
|
+
final analyticsServiceProvider = Provider<AnalyticsService>((ref) {
|
|
136
|
+
return AnalyticsService();
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 사용자 속성
|
|
141
|
+
|
|
142
|
+
```dart
|
|
143
|
+
/// 사용자 세그먼트 설정 (최대 25개 속성)
|
|
144
|
+
Future<void> setUserProperties({
|
|
145
|
+
required String userId,
|
|
146
|
+
String? userType,
|
|
147
|
+
String? subscriptionTier,
|
|
148
|
+
String? preferredSport,
|
|
149
|
+
String? region,
|
|
150
|
+
}) async {
|
|
151
|
+
final analytics = FirebaseAnalytics.instance;
|
|
152
|
+
|
|
153
|
+
await analytics.setUserId(id: userId);
|
|
154
|
+
|
|
155
|
+
if (userType != null) {
|
|
156
|
+
await analytics.setUserProperty(name: 'user_type', value: userType);
|
|
157
|
+
}
|
|
158
|
+
if (subscriptionTier != null) {
|
|
159
|
+
await analytics.setUserProperty(
|
|
160
|
+
name: 'subscription_tier',
|
|
161
|
+
value: subscriptionTier,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (preferredSport != null) {
|
|
165
|
+
await analytics.setUserProperty(
|
|
166
|
+
name: 'preferred_sport',
|
|
167
|
+
value: preferredSport,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (region != null) {
|
|
171
|
+
await analytics.setUserProperty(name: 'region', value: region);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### go_router 화면 추적 (AnalyticsObserver)
|
|
177
|
+
|
|
178
|
+
```dart
|
|
179
|
+
// GoRouter 설정에 observer 추가
|
|
180
|
+
final goRouter = GoRouter(
|
|
181
|
+
routes: [...],
|
|
182
|
+
observers: [
|
|
183
|
+
FirebaseAnalyticsObserver(
|
|
184
|
+
analytics: FirebaseAnalytics.instance,
|
|
185
|
+
nameExtractor: (settings) {
|
|
186
|
+
// route 이름을 screen_name으로 매핑
|
|
187
|
+
return settings.name ?? settings.arguments?.toString() ?? 'unknown';
|
|
188
|
+
},
|
|
189
|
+
),
|
|
190
|
+
],
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// GoRoute에 name 명시 (observer가 이름 추출)
|
|
194
|
+
GoRoute(
|
|
195
|
+
path: '/match/:id',
|
|
196
|
+
name: 'match_detail',
|
|
197
|
+
builder: (context, state) => MatchDetailScreen(
|
|
198
|
+
matchId: state.pathParameters['id']!,
|
|
199
|
+
),
|
|
200
|
+
),
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### 디버그 모드
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
# Android: DebugView 활성화
|
|
207
|
+
adb shell setprop debug.firebase.analytics.app <package_name>
|
|
208
|
+
|
|
209
|
+
# 이벤트가 실시간으로 Firebase Console > DebugView에 표시
|
|
210
|
+
# 비활성화:
|
|
211
|
+
adb shell setprop debug.firebase.analytics.app .none.
|
|
212
|
+
|
|
213
|
+
# iOS: Xcode Scheme > Arguments
|
|
214
|
+
-FIRDebugEnabled # 활성화
|
|
215
|
+
-FIRDebugDisabled # 비활성화
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 규칙
|
|
219
|
+
|
|
220
|
+
- 이벤트 택소노미 → 구현 전에 문서화 (이름, 파라미터, 트리거 조건)
|
|
221
|
+
- 이벤트 이름 → snake_case, 40자 이하, 자동 수집 이벤트와 충돌 금지
|
|
222
|
+
- 파라미터 → 최대 25개/이벤트, 값 100자 이하
|
|
223
|
+
- 사용자 속성 → 세그먼트 기준만 (최대 25개, 값 36자 이하)
|
|
224
|
+
- `AnalyticsObserver` → go_router 연동으로 화면 전환 자동 추적
|
|
225
|
+
- `setUserId` → 로그인 시 설정, 로그아웃 시 `null` 로 해제
|
|
226
|
+
- 디버그 → DebugView로 실시간 이벤트 검증 후 프로덕션 배포
|
|
227
|
+
- PII (개인식별정보) → 이벤트 파라미터에 이름/이메일/전화번호 금지
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Crashlytics Setup & Error Capture
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: "크래시 미수집 → 프로덕션 장애 감지 불가, 사용자 이탈 원인 파악 불가"
|
|
5
|
+
tags: crashlytics, firebase, crash, error, fatal
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Crashlytics Setup & Error Capture
|
|
9
|
+
|
|
10
|
+
**Impact: CRITICAL (크래시 미수집 → 프로덕션 장애 감지 불가, 사용자 이탈 원인 파악 불가)**
|
|
11
|
+
|
|
12
|
+
Firebase Crashlytics 초기화, Flutter/Dart 에러 캡처, 비치명 에러 기록,
|
|
13
|
+
사용자 식별자 및 커스텀 키 설정.
|
|
14
|
+
|
|
15
|
+
### 의존성
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# pubspec.yaml
|
|
19
|
+
dependencies:
|
|
20
|
+
firebase_core: ^3.8.0
|
|
21
|
+
firebase_crashlytics: ^4.3.0
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 초기화
|
|
25
|
+
|
|
26
|
+
**Incorrect (에러 핸들러 미연결):**
|
|
27
|
+
```dart
|
|
28
|
+
void main() async {
|
|
29
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
30
|
+
await Firebase.initializeApp();
|
|
31
|
+
// Crashlytics 에러 핸들러 미등록 → 크래시 미수집
|
|
32
|
+
runApp(const MyApp());
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Correct (완전한 에러 캡처 파이프라인):**
|
|
37
|
+
```dart
|
|
38
|
+
Future<void> main() async {
|
|
39
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
40
|
+
await Firebase.initializeApp();
|
|
41
|
+
|
|
42
|
+
// 1. Flutter 프레임워크 에러 (위젯 빌드 에러 등)
|
|
43
|
+
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
|
|
44
|
+
|
|
45
|
+
// 2. 비동기 에러 (Future, Isolate 등)
|
|
46
|
+
PlatformDispatcher.instance.onError = (error, stack) {
|
|
47
|
+
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
|
48
|
+
return true;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 3. Dart Zone 에러 (runZonedGuarded)
|
|
52
|
+
runZonedGuarded<Future<void>>(
|
|
53
|
+
() async {
|
|
54
|
+
runApp(const ProviderScope(child: MyApp()));
|
|
55
|
+
},
|
|
56
|
+
(error, stack) {
|
|
57
|
+
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 디버그 모드 제어
|
|
64
|
+
|
|
65
|
+
```dart
|
|
66
|
+
// 개발 중 Crashlytics 비활성화 (크래시 대시보드 오염 방지)
|
|
67
|
+
await FirebaseCrashlytics.instance
|
|
68
|
+
.setCrashlyticsCollectionEnabled(!kDebugMode);
|
|
69
|
+
|
|
70
|
+
// 또는 환경별 분리
|
|
71
|
+
final isProduction = const String.fromEnvironment('ENV') == 'production';
|
|
72
|
+
await FirebaseCrashlytics.instance
|
|
73
|
+
.setCrashlyticsCollectionEnabled(isProduction);
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 비치명 에러 기록
|
|
77
|
+
|
|
78
|
+
**Incorrect (try-catch만 하고 기록 안 함):**
|
|
79
|
+
```dart
|
|
80
|
+
try {
|
|
81
|
+
await apiService.fetchData();
|
|
82
|
+
} catch (e) {
|
|
83
|
+
debugPrint('Error: $e');
|
|
84
|
+
// → 프로덕션에서 에러 발생 사실을 알 수 없음
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Correct (비치명 에러 Crashlytics 기록):**
|
|
89
|
+
```dart
|
|
90
|
+
try {
|
|
91
|
+
await apiService.fetchData();
|
|
92
|
+
} catch (error, stack) {
|
|
93
|
+
// fatal: false → 비치명 에러 (앱 크래시 아님)
|
|
94
|
+
await FirebaseCrashlytics.instance.recordError(
|
|
95
|
+
error,
|
|
96
|
+
stack,
|
|
97
|
+
fatal: false,
|
|
98
|
+
reason: 'fetchData API call failed',
|
|
99
|
+
);
|
|
100
|
+
// 사용자에게 에러 UI 표시
|
|
101
|
+
rethrow; // 또는 적절한 에러 핸들링
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### 사용자 식별자
|
|
106
|
+
|
|
107
|
+
```dart
|
|
108
|
+
class CrashlyticsService {
|
|
109
|
+
final FirebaseCrashlytics _crashlytics = FirebaseCrashlytics.instance;
|
|
110
|
+
|
|
111
|
+
/// 로그인 시 사용자 식별자 설정
|
|
112
|
+
Future<void> setUser(String userId) async {
|
|
113
|
+
await _crashlytics.setUserIdentifier(userId);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// 로그아웃 시 식별자 제거
|
|
117
|
+
Future<void> clearUser() async {
|
|
118
|
+
await _crashlytics.setUserIdentifier('');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// 커스텀 키 설정 (크래시 컨텍스트)
|
|
122
|
+
Future<void> setContext({
|
|
123
|
+
required String appVersion,
|
|
124
|
+
required String buildNumber,
|
|
125
|
+
String? subscriptionTier,
|
|
126
|
+
bool? isOnboarded,
|
|
127
|
+
}) async {
|
|
128
|
+
await _crashlytics.setCustomKey('app_version', appVersion);
|
|
129
|
+
await _crashlytics.setCustomKey('build_number', buildNumber);
|
|
130
|
+
if (subscriptionTier != null) {
|
|
131
|
+
await _crashlytics.setCustomKey('subscription_tier', subscriptionTier);
|
|
132
|
+
}
|
|
133
|
+
if (isOnboarded != null) {
|
|
134
|
+
await _crashlytics.setCustomKey('is_onboarded', isOnboarded);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// 커스텀 로그 (크래시 발생 직전 흐름 추적)
|
|
139
|
+
void log(String message) {
|
|
140
|
+
_crashlytics.log(message);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// 비치명 에러 기록
|
|
144
|
+
Future<void> recordNonFatal(
|
|
145
|
+
dynamic error,
|
|
146
|
+
StackTrace stack, {
|
|
147
|
+
String? reason,
|
|
148
|
+
}) async {
|
|
149
|
+
await _crashlytics.recordError(
|
|
150
|
+
error,
|
|
151
|
+
stack,
|
|
152
|
+
fatal: false,
|
|
153
|
+
reason: reason,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Riverpod Provider
|
|
159
|
+
final crashlyticsServiceProvider = Provider<CrashlyticsService>((ref) {
|
|
160
|
+
return CrashlyticsService();
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### dSYM / ProGuard 설정
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
iOS (dSYM 업로드):
|
|
168
|
+
Xcode > Build Settings > Debug Information Format = DWARF with dSYM File
|
|
169
|
+
firebase_crashlytics가 빌드 스크립트 자동 추가 (Run Script Phase)
|
|
170
|
+
수동: firebase crashlytics:symbols:upload --app=<APP_ID> path/to/dSYMs
|
|
171
|
+
|
|
172
|
+
Android (ProGuard/R8 mapping):
|
|
173
|
+
android/app/build.gradle:
|
|
174
|
+
buildTypes {
|
|
175
|
+
release {
|
|
176
|
+
minifyEnabled true
|
|
177
|
+
shrinkResources true
|
|
178
|
+
// firebase_crashlytics Gradle 플러그인이 mapping 자동 업로드
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
android/build.gradle:
|
|
182
|
+
plugins { id 'com.google.firebase.crashlytics' }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 규칙
|
|
186
|
+
|
|
187
|
+
- `FlutterError.onError` → Crashlytics 연결 필수 (Flutter 프레임워크 에러)
|
|
188
|
+
- `PlatformDispatcher.instance.onError` → 비동기 에러 캡처 필수
|
|
189
|
+
- `runZonedGuarded` → Dart Zone 에러까지 포괄 (선택이지만 강력 권장)
|
|
190
|
+
- 디버그 모드 → `setCrashlyticsCollectionEnabled(false)` (대시보드 오염 방지)
|
|
191
|
+
- 비치명 에러 → `recordError(fatal: false)` + reason 명시
|
|
192
|
+
- `setUserIdentifier` → 로그인/로그아웃 시 설정/해제
|
|
193
|
+
- `setCustomKey` → 구독 등급, 기능 플래그 등 비즈니스 컨텍스트
|
|
194
|
+
- `log()` → 크래시 직전 사용자 흐름 기록 (최대 64KB)
|
|
195
|
+
- dSYM (iOS) / ProGuard mapping (Android) → 릴리스 빌드 심볼 업로드 필수
|