timsquad 3.3.0 → 3.5.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 +95 -5
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/full.js +1 -0
- package/dist/commands/full.js.map +1 -1
- package/dist/commands/git/pr.js +6 -5
- package/dist/commands/git/pr.js.map +1 -1
- package/dist/commands/git/release.js +2 -7
- package/dist/commands/git/release.js.map +1 -1
- package/dist/commands/improve.js +2 -2
- package/dist/commands/improve.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/log.d.ts.map +1 -1
- package/dist/commands/log.js +2 -2
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +6 -2
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/retro.js +8 -8
- package/dist/commands/retro.js.map +1 -1
- package/dist/commands/session.js +3 -3
- package/dist/commands/session.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 +228 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +23 -1
- 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/event-queue.d.ts.map +1 -1
- package/dist/daemon/event-queue.js +2 -2
- package/dist/daemon/event-queue.js.map +1 -1
- package/dist/daemon/index.d.ts +4 -2
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +214 -52
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jsonl-watcher.d.ts +1 -0
- package/dist/daemon/jsonl-watcher.d.ts.map +1 -1
- package/dist/daemon/jsonl-watcher.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-notes.d.ts +33 -0
- package/dist/daemon/session-notes.d.ts.map +1 -0
- package/dist/daemon/session-notes.js +74 -0
- package/dist/daemon/session-notes.js.map +1 -0
- package/dist/daemon/session-state.d.ts +27 -0
- package/dist/daemon/session-state.d.ts.map +1 -0
- package/dist/daemon/session-state.js +165 -0
- package/dist/daemon/session-state.js.map +1 -0
- package/dist/daemon/shutdown.d.ts.map +1 -1
- package/dist/daemon/shutdown.js +9 -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/agent-generator.d.ts +4 -0
- package/dist/lib/agent-generator.d.ts.map +1 -1
- package/dist/lib/agent-generator.js +52 -3
- package/dist/lib/agent-generator.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/project.d.ts.map +1 -1
- package/dist/lib/project.js +8 -3
- package/dist/lib/project.js.map +1 -1
- package/dist/lib/skill-generator.d.ts.map +1 -1
- package/dist/lib/skill-generator.js +22 -1
- package/dist/lib/skill-generator.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 +4 -4
- package/templates/base/agents/base/tsq-architect.md +2 -2
- package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
- package/templates/base/knowledge/checklists/plan-quality.md +31 -0
- package/templates/base/knowledge/checklists/stability-verification.md +14 -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/base/skills/stability-verification/SKILL.md +64 -0
- package/templates/base/skills/stability-verification/references/release-checklist.md +34 -0
- package/templates/base/skills/stability-verification/references/security-fix-patterns.md +112 -0
- package/templates/base/skills/stability-verification/rules/verification-layers.md +67 -0
- package/templates/base/skills/stability-verification/rules/verification-workflow.md +69 -0
- package/templates/base/skills/stability-verification/scripts/verify.sh +294 -0
- package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
- package/templates/platforms/claude-code/rules/build-gate.md +28 -0
- package/templates/platforms/claude-code/rules/completion-verification.md +30 -0
- package/templates/platforms/claude-code/rules/context-monitor.md +23 -0
- package/templates/platforms/claude-code/rules/plan-review.md +45 -0
- package/templates/platforms/claude-code/rules/quality-guards.md +43 -0
- package/templates/platforms/claude-code/rules/session-notes.md +18 -0
- package/templates/platforms/claude-code/rules/skill-suggest.md +27 -0
- package/templates/platforms/claude-code/scripts/build-gate.sh +73 -0
- package/templates/platforms/claude-code/scripts/completion-guard.sh +93 -0
- package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
- package/templates/platforms/claude-code/scripts/safe-guard.sh +83 -0
- package/templates/platforms/claude-code/scripts/skill-rules.json +85 -0
- package/templates/platforms/claude-code/scripts/skill-suggest.sh +105 -0
- package/templates/platforms/claude-code/settings.json +111 -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,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Performance Monitoring & Custom Traces
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: "성능 미측정 → 느린 화면 방치, ANR/프리징 원인 파악 불가"
|
|
5
|
+
tags: firebase-performance, trace, http-metric, frame, startup
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Performance Monitoring & Custom Traces
|
|
9
|
+
|
|
10
|
+
**Impact: HIGH (성능 미측정 → 느린 화면 방치, ANR/프리징 원인 파악 불가)**
|
|
11
|
+
|
|
12
|
+
Firebase Performance Monitoring 초기화, custom trace, HTTP metric 수집,
|
|
13
|
+
프레임 렌더링 모니터링, 앱 시작 시간 추적.
|
|
14
|
+
|
|
15
|
+
### 의존성
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# pubspec.yaml
|
|
19
|
+
dependencies:
|
|
20
|
+
firebase_core: ^3.8.0
|
|
21
|
+
firebase_performance: ^0.10.0
|
|
22
|
+
firebase_performance_dio: ^0.6.0 # dio 사용 시
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 초기화
|
|
26
|
+
|
|
27
|
+
```dart
|
|
28
|
+
Future<void> main() async {
|
|
29
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
30
|
+
await Firebase.initializeApp();
|
|
31
|
+
|
|
32
|
+
// Performance Monitoring은 별도 초기화 불필요 (Firebase init 시 자동 활성화)
|
|
33
|
+
// 디버그 모드에서 비활성화 (선택)
|
|
34
|
+
if (kDebugMode) {
|
|
35
|
+
await FirebasePerformance.instance
|
|
36
|
+
.setPerformanceCollectionEnabled(false);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
runApp(const ProviderScope(child: MyApp()));
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Custom Trace
|
|
44
|
+
|
|
45
|
+
**Incorrect (시작/종료 미매칭):**
|
|
46
|
+
```dart
|
|
47
|
+
final trace = FirebasePerformance.instance.newTrace('load_data');
|
|
48
|
+
await trace.start();
|
|
49
|
+
await fetchData();
|
|
50
|
+
// trace.stop() 누락 → 데이터 미전송, 메모리 누수
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Correct (안전한 trace 래핑):**
|
|
54
|
+
```dart
|
|
55
|
+
class PerformanceService {
|
|
56
|
+
final FirebasePerformance _performance = FirebasePerformance.instance;
|
|
57
|
+
|
|
58
|
+
/// 안전한 trace 실행 (자동 start/stop)
|
|
59
|
+
Future<T> trace<T>(
|
|
60
|
+
String name,
|
|
61
|
+
Future<T> Function() operation, {
|
|
62
|
+
Map<String, String>? attributes,
|
|
63
|
+
Map<String, int>? metrics,
|
|
64
|
+
}) async {
|
|
65
|
+
final trace = _performance.newTrace(name);
|
|
66
|
+
|
|
67
|
+
// 속성 설정 (최대 5개)
|
|
68
|
+
attributes?.forEach((key, value) {
|
|
69
|
+
trace.putAttribute(key, value);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await trace.start();
|
|
73
|
+
try {
|
|
74
|
+
final result = await operation();
|
|
75
|
+
|
|
76
|
+
// 메트릭 설정 (최대 32개)
|
|
77
|
+
metrics?.forEach((key, value) {
|
|
78
|
+
trace.setMetric(key, value);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
trace.putAttribute('error', e.runtimeType.toString());
|
|
84
|
+
rethrow;
|
|
85
|
+
} finally {
|
|
86
|
+
await trace.stop(); // 항상 stop 보장
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 사용 예시
|
|
92
|
+
final perfService = ref.read(performanceServiceProvider);
|
|
93
|
+
final matches = await perfService.trace(
|
|
94
|
+
'fetch_matches',
|
|
95
|
+
() => matchRepository.getMatches(page: 1),
|
|
96
|
+
attributes: {'sport': 'tennis', 'region': 'sg'},
|
|
97
|
+
metrics: {'page': 1},
|
|
98
|
+
);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 핵심 플로우 trace 예시
|
|
102
|
+
|
|
103
|
+
```dart
|
|
104
|
+
/// 앱 시작 시간 추적
|
|
105
|
+
class AppStartupTrace {
|
|
106
|
+
static Trace? _trace;
|
|
107
|
+
|
|
108
|
+
static Future<void> start() async {
|
|
109
|
+
_trace = FirebasePerformance.instance.newTrace('app_startup');
|
|
110
|
+
await _trace?.start();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static Future<void> markMilestone(String name) async {
|
|
114
|
+
_trace?.putAttribute(name, DateTime.now().toIso8601String());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static Future<void> stop() async {
|
|
118
|
+
await _trace?.stop();
|
|
119
|
+
_trace = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// main.dart
|
|
124
|
+
Future<void> main() async {
|
|
125
|
+
await AppStartupTrace.start();
|
|
126
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
127
|
+
await AppStartupTrace.markMilestone('binding_initialized');
|
|
128
|
+
|
|
129
|
+
await Firebase.initializeApp();
|
|
130
|
+
await AppStartupTrace.markMilestone('firebase_initialized');
|
|
131
|
+
|
|
132
|
+
runApp(const ProviderScope(child: MyApp()));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 첫 화면 렌더링 완료 시
|
|
136
|
+
class HomeScreen extends ConsumerStatefulWidget {
|
|
137
|
+
@override
|
|
138
|
+
ConsumerState<HomeScreen> createState() => _HomeScreenState();
|
|
139
|
+
|
|
140
|
+
@override
|
|
141
|
+
void initState() {
|
|
142
|
+
super.initState();
|
|
143
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
144
|
+
AppStartupTrace.stop(); // 첫 프레임 렌더링 완료
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### HTTP Metric (Dio Interceptor)
|
|
151
|
+
|
|
152
|
+
```dart
|
|
153
|
+
/// Dio에 Performance interceptor 추가
|
|
154
|
+
final dio = Dio()
|
|
155
|
+
..interceptors.add(
|
|
156
|
+
DioFirebasePerformanceInterceptor(), // firebase_performance_dio
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
/// 또는 수동 HTTP metric
|
|
160
|
+
class PerformanceInterceptor extends Interceptor {
|
|
161
|
+
final Map<String, HttpMetric> _metrics = {};
|
|
162
|
+
|
|
163
|
+
@override
|
|
164
|
+
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
|
165
|
+
final metric = FirebasePerformance.instance.newHttpMetric(
|
|
166
|
+
options.uri.toString(),
|
|
167
|
+
_toHttpMethod(options.method),
|
|
168
|
+
);
|
|
169
|
+
metric.start();
|
|
170
|
+
_metrics[options.hashCode.toString()] = metric;
|
|
171
|
+
handler.next(options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@override
|
|
175
|
+
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
|
176
|
+
final metric = _metrics.remove(response.requestOptions.hashCode.toString());
|
|
177
|
+
if (metric != null) {
|
|
178
|
+
metric
|
|
179
|
+
..responseContentType = response.headers.value('content-type')
|
|
180
|
+
..httpResponseCode = response.statusCode
|
|
181
|
+
..responsePayloadSize = response.data.toString().length;
|
|
182
|
+
metric.stop();
|
|
183
|
+
}
|
|
184
|
+
handler.next(response);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@override
|
|
188
|
+
void onError(DioException err, ErrorInterceptorHandler handler) {
|
|
189
|
+
final metric = _metrics.remove(err.requestOptions.hashCode.toString());
|
|
190
|
+
if (metric != null) {
|
|
191
|
+
metric.httpResponseCode = err.response?.statusCode;
|
|
192
|
+
metric.putAttribute('error', err.type.name);
|
|
193
|
+
metric.stop();
|
|
194
|
+
}
|
|
195
|
+
handler.next(err);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
HttpMethod _toHttpMethod(String method) {
|
|
199
|
+
return switch (method.toUpperCase()) {
|
|
200
|
+
'GET' => HttpMethod.Get,
|
|
201
|
+
'POST' => HttpMethod.Post,
|
|
202
|
+
'PUT' => HttpMethod.Put,
|
|
203
|
+
'DELETE' => HttpMethod.Delete,
|
|
204
|
+
'PATCH' => HttpMethod.Patch,
|
|
205
|
+
_ => HttpMethod.Get,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 프레임 렌더링 모니터링
|
|
212
|
+
|
|
213
|
+
```dart
|
|
214
|
+
/// 느린 프레임 감지 (개발/QA용)
|
|
215
|
+
class FrameMonitor {
|
|
216
|
+
static void start() {
|
|
217
|
+
if (!kDebugMode) return;
|
|
218
|
+
|
|
219
|
+
WidgetsBinding.instance.addTimingsCallback((timings) {
|
|
220
|
+
for (final timing in timings) {
|
|
221
|
+
// 60fps 기준: 16.67ms 이상이면 느린 프레임
|
|
222
|
+
final buildDuration = timing.buildDuration.inMilliseconds;
|
|
223
|
+
final rasterDuration = timing.rasterDuration.inMilliseconds;
|
|
224
|
+
final totalDuration = timing.totalSpan.inMilliseconds;
|
|
225
|
+
|
|
226
|
+
if (totalDuration > 16) {
|
|
227
|
+
debugPrint(
|
|
228
|
+
'Slow frame: total=${totalDuration}ms '
|
|
229
|
+
'build=${buildDuration}ms raster=${rasterDuration}ms',
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### 규칙
|
|
239
|
+
|
|
240
|
+
- Custom trace → `start()`/`stop()` 반드시 매칭 (try-finally 패턴)
|
|
241
|
+
- Trace 이름 → 소문자 + 언더스코어, 100자 이하
|
|
242
|
+
- Trace 속성 → 최대 5개 (key 32자, value 100자)
|
|
243
|
+
- Trace 메트릭 → 최대 32개 (정수값)
|
|
244
|
+
- HTTP metric → dio interceptor 또는 수동 래핑으로 API 응답 시간 수집
|
|
245
|
+
- 앱 시작 trace → main() 시작 ~ 첫 프레임 렌더링 완료
|
|
246
|
+
- 핵심 플로우 (로그인, 검색, 매치 생성) → custom trace 적용
|
|
247
|
+
- 디버그 모드 → `setPerformanceCollectionEnabled(false)` (선택)
|
|
248
|
+
- 프레임 모니터링 → 16ms (60fps) / 8ms (120fps) 초과 시 경고
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Sentry Integration & Breadcrumbs
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: "에러 컨텍스트 부족 → 재현 불가, 크래시 원인 추적 지연"
|
|
5
|
+
tags: sentry, breadcrumb, scope, dsn, error-tracking
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Sentry Integration & Breadcrumbs
|
|
9
|
+
|
|
10
|
+
**Impact: MEDIUM (에러 컨텍스트 부족 → 재현 불가, 크래시 원인 추적 지연)**
|
|
11
|
+
|
|
12
|
+
sentry_flutter 초기화, DSN 환경별 분리, breadcrumbs 자동/수동 기록,
|
|
13
|
+
scope 설정, Crashlytics와의 공존 전략.
|
|
14
|
+
|
|
15
|
+
### 의존성
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
# pubspec.yaml
|
|
19
|
+
dependencies:
|
|
20
|
+
sentry_flutter: ^8.12.0
|
|
21
|
+
sentry_dio: ^8.12.0 # dio HTTP breadcrumbs
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 초기화
|
|
25
|
+
|
|
26
|
+
**Incorrect (하드코딩된 DSN, 기본 설정만):**
|
|
27
|
+
```dart
|
|
28
|
+
await SentryFlutter.init((options) {
|
|
29
|
+
options.dsn = 'https://key@sentry.io/123'; // 하드코딩 → 환경 구분 불가
|
|
30
|
+
});
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Correct (환경별 DSN + 상세 설정):**
|
|
34
|
+
```dart
|
|
35
|
+
Future<void> main() async {
|
|
36
|
+
WidgetsFlutterBinding.ensureInitialized();
|
|
37
|
+
await Firebase.initializeApp();
|
|
38
|
+
|
|
39
|
+
await SentryFlutter.init(
|
|
40
|
+
(options) {
|
|
41
|
+
// DSN 환경별 분리 (--dart-define으로 주입)
|
|
42
|
+
options.dsn = const String.fromEnvironment(
|
|
43
|
+
'SENTRY_DSN',
|
|
44
|
+
defaultValue: '', // 빈 문자열 → Sentry 비활성화
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// 환경 태그
|
|
48
|
+
options.environment = const String.fromEnvironment(
|
|
49
|
+
'ENV',
|
|
50
|
+
defaultValue: 'development',
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// 샘플링 (프로덕션에서 100%는 비용 과다)
|
|
54
|
+
options.tracesSampleRate = kDebugMode ? 1.0 : 0.3;
|
|
55
|
+
options.profilesSampleRate = kDebugMode ? 1.0 : 0.1;
|
|
56
|
+
|
|
57
|
+
// 릴리스 버전 (소스맵 매핑)
|
|
58
|
+
options.release = '${packageInfo.version}+${packageInfo.buildNumber}';
|
|
59
|
+
|
|
60
|
+
// 디버그 모드
|
|
61
|
+
options.debug = kDebugMode;
|
|
62
|
+
|
|
63
|
+
// 자동 breadcrumb 수집
|
|
64
|
+
options.enableAutoNativeBreadcrumbs = true;
|
|
65
|
+
options.enableAutoPerformanceTracing = true;
|
|
66
|
+
|
|
67
|
+
// PII 전송 비활성화
|
|
68
|
+
options.sendDefaultPii = false;
|
|
69
|
+
|
|
70
|
+
// 에러 필터링 (무시할 에러 타입)
|
|
71
|
+
options.beforeSend = (event, hint) {
|
|
72
|
+
// 네트워크 끊김 에러는 무시
|
|
73
|
+
if (event.throwable is SocketException) return null;
|
|
74
|
+
return event;
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
appRunner: () => runApp(
|
|
78
|
+
const ProviderScope(child: MyApp()),
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Breadcrumbs
|
|
85
|
+
|
|
86
|
+
```dart
|
|
87
|
+
class SentryBreadcrumbService {
|
|
88
|
+
/// 네비게이션 breadcrumb (수동 — go_router 연동)
|
|
89
|
+
static void navigationBreadcrumb({
|
|
90
|
+
required String from,
|
|
91
|
+
required String to,
|
|
92
|
+
}) {
|
|
93
|
+
Sentry.addBreadcrumb(Breadcrumb(
|
|
94
|
+
type: 'navigation',
|
|
95
|
+
category: 'navigation',
|
|
96
|
+
data: {'from': from, 'to': to},
|
|
97
|
+
));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// 사용자 액션 breadcrumb
|
|
101
|
+
static void userActionBreadcrumb({
|
|
102
|
+
required String action,
|
|
103
|
+
required String target,
|
|
104
|
+
Map<String, dynamic>? data,
|
|
105
|
+
}) {
|
|
106
|
+
Sentry.addBreadcrumb(Breadcrumb(
|
|
107
|
+
type: 'user',
|
|
108
|
+
category: 'user.action',
|
|
109
|
+
message: '$action on $target',
|
|
110
|
+
data: data,
|
|
111
|
+
));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// 상태 변경 breadcrumb
|
|
115
|
+
static void stateBreadcrumb({
|
|
116
|
+
required String category,
|
|
117
|
+
required String message,
|
|
118
|
+
Map<String, dynamic>? data,
|
|
119
|
+
}) {
|
|
120
|
+
Sentry.addBreadcrumb(Breadcrumb(
|
|
121
|
+
type: 'info',
|
|
122
|
+
category: category,
|
|
123
|
+
message: message,
|
|
124
|
+
data: data,
|
|
125
|
+
level: SentryLevel.info,
|
|
126
|
+
));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// go_router NavigatorObserver로 자동 네비게이션 breadcrumb
|
|
131
|
+
class SentryNavigatorObserver extends NavigatorObserver {
|
|
132
|
+
@override
|
|
133
|
+
void didPush(Route route, Route? previousRoute) {
|
|
134
|
+
SentryBreadcrumbService.navigationBreadcrumb(
|
|
135
|
+
from: previousRoute?.settings.name ?? 'unknown',
|
|
136
|
+
to: route.settings.name ?? 'unknown',
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@override
|
|
141
|
+
void didPop(Route route, Route? previousRoute) {
|
|
142
|
+
SentryBreadcrumbService.navigationBreadcrumb(
|
|
143
|
+
from: route.settings.name ?? 'unknown',
|
|
144
|
+
to: previousRoute?.settings.name ?? 'unknown',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Scope 설정
|
|
151
|
+
|
|
152
|
+
```dart
|
|
153
|
+
/// 사용자 정보 + 태그 설정
|
|
154
|
+
class SentryScopeManager {
|
|
155
|
+
/// 로그인 시 사용자 설정
|
|
156
|
+
static Future<void> setUser({
|
|
157
|
+
required String id,
|
|
158
|
+
String? email,
|
|
159
|
+
String? username,
|
|
160
|
+
Map<String, String>? extras,
|
|
161
|
+
}) async {
|
|
162
|
+
Sentry.configureScope((scope) {
|
|
163
|
+
scope.setUser(SentryUser(
|
|
164
|
+
id: id,
|
|
165
|
+
email: email, // PII — sendDefaultPii: true 필요
|
|
166
|
+
username: username,
|
|
167
|
+
));
|
|
168
|
+
// 태그 (필터링/검색용)
|
|
169
|
+
scope.setTag('subscription_tier', extras?['tier'] ?? 'free');
|
|
170
|
+
scope.setTag('region', extras?['region'] ?? 'unknown');
|
|
171
|
+
// 추가 데이터 (상세 컨텍스트)
|
|
172
|
+
extras?.forEach((key, value) {
|
|
173
|
+
scope.setExtra(key, value);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// 로그아웃 시 정리
|
|
179
|
+
static Future<void> clearUser() async {
|
|
180
|
+
Sentry.configureScope((scope) {
|
|
181
|
+
scope.setUser(null);
|
|
182
|
+
scope.removeTag('subscription_tier');
|
|
183
|
+
scope.removeTag('region');
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Dio HTTP Breadcrumbs
|
|
190
|
+
|
|
191
|
+
```dart
|
|
192
|
+
/// sentry_dio로 HTTP 요청/응답 자동 breadcrumb
|
|
193
|
+
final dio = Dio()
|
|
194
|
+
..addSentry(); // sentry_dio 확장 메서드
|
|
195
|
+
|
|
196
|
+
// 또는 수동 interceptor
|
|
197
|
+
class SentryDioInterceptor extends Interceptor {
|
|
198
|
+
@override
|
|
199
|
+
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
|
200
|
+
Sentry.addBreadcrumb(Breadcrumb(
|
|
201
|
+
type: 'http',
|
|
202
|
+
category: 'http',
|
|
203
|
+
data: {
|
|
204
|
+
'url': response.requestOptions.uri.toString(),
|
|
205
|
+
'method': response.requestOptions.method,
|
|
206
|
+
'status_code': response.statusCode,
|
|
207
|
+
'duration_ms': response.requestOptions.extra['start_time'] != null
|
|
208
|
+
? DateTime.now().difference(
|
|
209
|
+
response.requestOptions.extra['start_time'] as DateTime,
|
|
210
|
+
).inMilliseconds
|
|
211
|
+
: null,
|
|
212
|
+
},
|
|
213
|
+
));
|
|
214
|
+
handler.next(response);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Crashlytics + Sentry 공존 전략
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
역할 분리:
|
|
223
|
+
┌──────────────┬────────────────────────┬──────────────────────┐
|
|
224
|
+
│ │ Firebase Crashlytics │ Sentry │
|
|
225
|
+
├──────────────┼────────────────────────┼──────────────────────┤
|
|
226
|
+
│ 강점 │ 크래시 집계, 안정성 % │ 에러 컨텍스트, 검색 │
|
|
227
|
+
│ 크래시 │ O (primary) │ O (backup) │
|
|
228
|
+
│ 비치명 에러 │ O (recordError) │ O (captureException) │
|
|
229
|
+
│ Breadcrumbs │ X (log만 가능) │ O (풍부한 컨텍스트) │
|
|
230
|
+
│ 알림 │ Firebase Console │ Slack/Email 통합 │
|
|
231
|
+
│ 비용 │ 무료 │ 무료 (5K 이벤트/월) │
|
|
232
|
+
└──────────────┴────────────────────────┴──────────────────────┘
|
|
233
|
+
|
|
234
|
+
공존 시 주의:
|
|
235
|
+
- 에러를 양쪽 모두에 전송하면 이벤트 쿼터 소모 → 역할 기반 필터링
|
|
236
|
+
- Crashlytics = 크래시 대시보드 (안정성 %)
|
|
237
|
+
- Sentry = 에러 디버깅 (breadcrumb, context, 검색)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 규칙
|
|
241
|
+
|
|
242
|
+
- DSN → `--dart-define`으로 환경별 주입, 하드코딩 금지
|
|
243
|
+
- `tracesSampleRate` → 프로덕션 0.1~0.3 (비용 절감)
|
|
244
|
+
- `beforeSend` → 불필요한 에러 (SocketException 등) 필터링
|
|
245
|
+
- Breadcrumb → 네비게이션, HTTP, 사용자 액션 자동 기록
|
|
246
|
+
- Scope → 로그인/로그아웃 시 사용자 정보 설정/해제
|
|
247
|
+
- PII → `sendDefaultPii: false` 기본, 필요 시 명시적 활성화
|
|
248
|
+
- Crashlytics 공존 → 각 도구의 강점에 맞는 역할 분리
|
|
249
|
+
- 릴리스 → `options.release` 설정 + 소스맵/디버그 심볼 업로드
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: networking
|
|
3
|
+
description: |
|
|
4
|
+
Flutter 네트워크 통신 가이드라인.
|
|
5
|
+
Dio HTTP 클라이언트, Retrofit 코드 생성, 인터셉터 체인,
|
|
6
|
+
에러 핸들링, 연결 상태 관리, 캐시 전략.
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
tags: [flutter, dio, retrofit, http, api, interceptor, connectivity]
|
|
9
|
+
user-invocable: false
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Networking
|
|
13
|
+
|
|
14
|
+
Flutter Dio + Retrofit + Interceptor + Connectivity 통합 가이드.
|
|
15
|
+
HTTP 클라이언트 설정부터 오프라인 캐시까지, API 통신 전체 파이프라인.
|
|
16
|
+
|
|
17
|
+
## Philosophy
|
|
18
|
+
|
|
19
|
+
- API 클라이언트는 서비스 — Feature-first 구조에서 `core/networking/` 중앙화
|
|
20
|
+
- 에러는 도메인 타입 — DioException을 앱 도메인 에러(NetworkFailure)로 변환
|
|
21
|
+
- 오프라인은 기본 — connectivity 감지 + 캐시 + 재시도를 기본 탑재
|
|
22
|
+
- 인터셉터는 레이어 — 인증, 재시도, 로깅, 에러 변환을 체인으로 분리
|
|
23
|
+
|
|
24
|
+
## Resources
|
|
25
|
+
|
|
26
|
+
6개 규칙 + 1개 참조. 네트워크 통신 전체를 커버.
|
|
27
|
+
|
|
28
|
+
| Priority | Type | Resource | Description |
|
|
29
|
+
|----------|------|----------|-------------|
|
|
30
|
+
| CRITICAL | rule | [dio-setup](rules/dio-setup.md) | Dio 싱글톤, BaseOptions, Riverpod Provider |
|
|
31
|
+
| CRITICAL | rule | [interceptors](rules/interceptors.md) | Interceptor chain 순서: Auth → Retry → Logging → Error |
|
|
32
|
+
| CRITICAL | rule | [error-handling](rules/error-handling.md) | DioException → NetworkFailure sealed class, Result 패턴 |
|
|
33
|
+
| HIGH | rule | [retrofit-patterns](rules/retrofit-patterns.md) | @RestApi, 코드 생성, freezed 모델 연동 |
|
|
34
|
+
| HIGH | rule | [connectivity](rules/connectivity.md) | connectivity_plus, 오프라인 큐, 자동 재시도 |
|
|
35
|
+
| MEDIUM | rule | [caching](rules/caching.md) | dio_cache_interceptor, ETag, 오프라인 fallback |
|
|
36
|
+
| — | ref | [api-client-architecture](references/api-client-architecture.md) | 디렉토리 구조, Provider 구성, Repository 연동, Mock 테스트 |
|
|
37
|
+
|
|
38
|
+
## Quick Rules
|
|
39
|
+
|
|
40
|
+
### Dio 설정
|
|
41
|
+
- `Dio(BaseOptions(...))` 싱글톤 — Riverpod Provider로 전역 관리
|
|
42
|
+
- `connectTimeout: 15s`, `receiveTimeout: 15s` 기본값
|
|
43
|
+
- `baseUrl` 은 환경별 분리 (dev/staging/prod)
|
|
44
|
+
- `contentType: 'application/json'` 기본 헤더
|
|
45
|
+
|
|
46
|
+
### 인터셉터 체인
|
|
47
|
+
- 순서: Auth → Retry → Logging → Error Transform
|
|
48
|
+
- Auth: 토큰 주입 + 401 시 리프레시
|
|
49
|
+
- Retry: 지수 백오프, 최대 3회, 5xx/timeout만
|
|
50
|
+
- Logging: debug 빌드에서만 활성화
|
|
51
|
+
|
|
52
|
+
### 에러 핸들링
|
|
53
|
+
- `DioException` → `NetworkFailure` sealed class 변환
|
|
54
|
+
- `connectionTimeout/sendTimeout` → `TimeoutFailure`
|
|
55
|
+
- `badResponse` → `ServerFailure(statusCode, message)`
|
|
56
|
+
- `connectionError` → `NoConnectionFailure`
|
|
57
|
+
- Repository에서 `Result<T, Failure>` 패턴 반환
|
|
58
|
+
|
|
59
|
+
### Retrofit 코드 생성
|
|
60
|
+
- `@RestApi(baseUrl: '')` — baseUrl은 Dio에서 관리
|
|
61
|
+
- 응답 모델은 freezed + json_serializable
|
|
62
|
+
- `build_runner watch` 로 개발 중 자동 생성
|
|
63
|
+
|
|
64
|
+
### 연결 상태
|
|
65
|
+
- `connectivity_plus` → 네트워크 상태 실시간 감지
|
|
66
|
+
- 오프라인 시 요청 큐잉, 연결 복구 시 자동 재시도
|
|
67
|
+
- UI에 오프라인 배너 표시
|
|
68
|
+
|
|
69
|
+
### 캐시
|
|
70
|
+
- `dio_cache_interceptor` → GET 응답 캐시
|
|
71
|
+
- `ETag`/`Last-Modified` 헤더 활용
|
|
72
|
+
- 오프라인 시 캐시 fallback
|
|
73
|
+
|
|
74
|
+
## Checklist
|
|
75
|
+
|
|
76
|
+
| Priority | Item |
|
|
77
|
+
|----------|------|
|
|
78
|
+
| CRITICAL | Dio 싱글톤 인스턴스 Riverpod Provider로 관리 |
|
|
79
|
+
| CRITICAL | Interceptor 순서: Auth → Retry → Logging → Error Transform |
|
|
80
|
+
| CRITICAL | DioException → 도메인 NetworkFailure 변환 처리 |
|
|
81
|
+
| HIGH | Auth 인터셉터에서 401 → 토큰 리프레시 → 재요청 |
|
|
82
|
+
| HIGH | Retry 인터셉터: 지수 백오프, 5xx/timeout만, 최대 3회 |
|
|
83
|
+
| HIGH | Retrofit @RestApi 응답 타입을 freezed 모델로 정의 |
|
|
84
|
+
| HIGH | connectivity_plus로 오프라인 감지 + UI 배너 |
|
|
85
|
+
| MEDIUM | 오프라인 요청 큐 + 연결 복구 시 자동 재시도 |
|
|
86
|
+
| MEDIUM | GET 응답 캐시 (dio_cache_interceptor) |
|
|
87
|
+
| MEDIUM | 환경별 baseUrl 분리 (dev/staging/prod) |
|
|
88
|
+
| MEDIUM | Release 빌드에서 Logging 인터셉터 비활성화 |
|