|
7 | 7 | - `dart compile exe bin/raygun_cli.dart -o raygun-cli` - Build executable |
8 | 8 | - `dart run bin/raygun_cli.dart` - Run CLI locally |
9 | 9 | - `dart format .` - Format code |
| 10 | +- `dart run build_runner build` - Generate mocks (one-time) |
| 11 | +- `dart run build_runner watch` - Auto-regenerate mocks on changes |
10 | 12 |
|
11 | 13 | ## Architecture |
12 | 14 | - **CLI Tool**: Uploads sourcemaps, manages obfuscation symbols, tracks deployments for Raygun.com |
13 | 15 | - **Main Entry**: `bin/raygun_cli.dart` - CLI argument parsing and command routing |
14 | | -- **Commands**: `lib/src/` - Four main command modules: sourcemap, symbols, deployments, proguard |
| 16 | +- **Commands**: `lib/src/` - Five main command modules: sourcemap, symbols, deployments, proguard, dsym |
15 | 17 | - **APIs**: Each command has corresponding API client (`*_api.dart`) for Raygun REST API calls |
16 | 18 | - **Config**: `config_props.dart` handles arg parsing with env var fallbacks (RAYGUN_APP_ID, RAYGUN_TOKEN, RAYGUN_API_KEY) |
17 | 19 |
|
| 20 | +### Directory Structure |
| 21 | +``` |
| 22 | +lib/src/[command]/ |
| 23 | + ├── [command]_command.dart # CLI parsing and routing |
| 24 | + ├── [command]_api.dart # Raygun API integration |
| 25 | + └── [command].dart # Business logic (if complex) |
| 26 | +lib/src/core/ # Shared utilities (RaygunCommand, RaygunApi builders) |
| 27 | +test/[command]/ # Tests mirror lib structure |
| 28 | +``` |
| 29 | + |
| 30 | +### Dependency Flow |
| 31 | +``` |
| 32 | +bin/raygun_cli.dart → lib/src/[command]/[command]_command.dart → *_api.dart |
| 33 | +``` |
| 34 | +- Commands are stateless; API clients handle HTTP |
| 35 | +- Commands receive API clients via constructor (dependency injection) |
| 36 | + |
18 | 37 | ## Code Style |
19 | 38 | - **Imports**: Standard library first, then package imports, then relative imports |
20 | 39 | - **Naming**: Snake_case for files/dirs, camelCase for variables, PascalCase for classes |
|
23 | 42 | - **Types**: Use explicit types for public APIs, required named parameters preferred |
24 | 43 | - **Strings**: Use single quotes, string interpolation with $variable or ${expression} |
25 | 44 | - **Comments**: Use /// for public API documentation, avoid inline comments |
| 45 | + |
| 46 | +## Error Handling & Exit Codes |
| 47 | +Standard error handling patterns used throughout: |
| 48 | +- **Exit code 0**: Success |
| 49 | +- **Exit code 1**: Failure (operation didn't succeed) |
| 50 | +- **Exit code 2**: Error (exception or invalid input) |
| 51 | +- Print errors to console before returning/exiting |
| 52 | +- Use `Future<bool>` return type for async operations |
| 53 | +- Chain with `.then()` and `.catchError()` for error handling |
| 54 | + |
| 55 | +Example: |
| 56 | +```dart |
| 57 | +run().then((result) { |
| 58 | + if (result) { |
| 59 | + exit(0); |
| 60 | + } else { |
| 61 | + exit(2); |
| 62 | + } |
| 63 | +}).catchError((e) { |
| 64 | + print('Error: $e'); |
| 65 | + exit(2); |
| 66 | +}); |
| 67 | +``` |
| 68 | + |
| 69 | +## Command Implementation Patterns |
| 70 | +All commands extend the `RaygunCommand` abstract class: |
| 71 | + |
| 72 | +### Required Implementation |
| 73 | +```dart |
| 74 | +class MyCommand extends RaygunCommand { |
| 75 | + const MyCommand({required this.api}); |
| 76 | +
|
| 77 | + final MyApi api; |
| 78 | +
|
| 79 | + @override |
| 80 | + String get name => 'mycommand'; |
| 81 | +
|
| 82 | + @override |
| 83 | + ArgParser buildParser() { /* ... */ } |
| 84 | +
|
| 85 | + @override |
| 86 | + void execute(ArgResults command, bool verbose) { /* ... */ } |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +### Command Patterns |
| 91 | +- **Help Flag**: Always check for `--help` flag first and exit(0) |
| 92 | +- **Config Loading**: Use `ConfigProp.load()` for app-id, token, api-key (exits if missing) |
| 93 | +- **Subcommands**: Use `ArgParser.addCommand()` (see symbols: upload/list/delete) |
| 94 | +- **Verbose Flag**: Available in all commands (inherited from main parser) |
| 95 | +- **Mandatory Args**: Use `mandatory: true` in ArgParser for required flags |
| 96 | + |
| 97 | +### Example: Subcommands Pattern (symbols command) |
| 98 | +```dart |
| 99 | +ArgParser buildParser() { |
| 100 | + return ArgParser() |
| 101 | + ..addOption('app-id') |
| 102 | + ..addOption('token') |
| 103 | + ..addCommand('upload') |
| 104 | + ..addCommand('list') |
| 105 | + ..addCommand('delete'); |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +## API Client Patterns |
| 110 | +Each command has a corresponding API client: |
| 111 | + |
| 112 | +### Builder Pattern for Requests |
| 113 | +- `RaygunMultipartRequestBuilder` - For file uploads |
| 114 | +- `RaygunPostRequestBuilder` - For JSON POST requests |
| 115 | + |
| 116 | +Example: |
| 117 | +```dart |
| 118 | +final request = RaygunMultipartRequestBuilder(url, 'POST') |
| 119 | + .addBearerToken(token) |
| 120 | + .addFile('file', filePath) |
| 121 | + .addField('version', version) |
| 122 | + .build(); |
| 123 | +``` |
| 124 | + |
| 125 | +### API Client Structure |
| 126 | +- Factory method pattern: Use static `.create()` for production instances |
| 127 | +- Return `Future<bool>` to indicate success/failure |
| 128 | +- Print response codes and messages for debugging |
| 129 | +- Handle HTTP responses and errors appropriately |
| 130 | + |
| 131 | +## Testing Patterns & Mock Generation |
| 132 | + |
| 133 | +### Test Structure |
| 134 | +- Tests use `mockito` for mocking API clients |
| 135 | +- Test files mirror `lib/` structure (e.g., `lib/src/symbols/` → `test/symbols/`) |
| 136 | +- Mock files use `.mocks.dart` suffix and are git-tracked |
| 137 | +- Generate mocks with: `dart run build_runner build` |
| 138 | + |
| 139 | +### Test Pattern |
| 140 | +```dart |
| 141 | +// 1. Generate mock classes with @GenerateMocks annotation |
| 142 | +@GenerateMocks([MyApi]) |
| 143 | +void main() { |
| 144 | + group('MyCommand', () { |
| 145 | + late MockMyApi mockApi; |
| 146 | +
|
| 147 | + setUp(() { |
| 148 | + mockApi = MockMyApi(); |
| 149 | + }); |
| 150 | +
|
| 151 | + test('description', () async { |
| 152 | + // 2. Setup mock behavior |
| 153 | + when(mockApi.someMethod()).thenAnswer((_) async => true); |
| 154 | +
|
| 155 | + // 3. Inject mock into command |
| 156 | + final command = MyCommand(api: mockApi); |
| 157 | +
|
| 158 | + // 4. Execute and verify |
| 159 | + final result = await command.run(...); |
| 160 | + expect(result, true); |
| 161 | + }); |
| 162 | + }); |
| 163 | +} |
| 164 | +``` |
| 165 | + |
| 166 | +### Mock Regeneration |
| 167 | +- After changing API signatures, run: `dart run build_runner build` |
| 168 | +- Use `watch` mode during development: `dart run build_runner watch` |
| 169 | + |
| 170 | +## Dependency Injection |
| 171 | + |
| 172 | +### Pattern |
| 173 | +- Commands receive API clients via constructor |
| 174 | +- Global command instances use `.create()` factories |
| 175 | +- Tests inject mock API clients |
| 176 | +- Enables testing without hitting real APIs |
| 177 | + |
| 178 | +Example: |
| 179 | +```dart |
| 180 | +// Production usage (in command file) |
| 181 | +SymbolsCommand symbolsCommand = SymbolsCommand(api: SymbolsApi.create()); |
| 182 | +
|
| 183 | +// Test usage |
| 184 | +final mockApi = MockSymbolsApi(); |
| 185 | +final command = SymbolsCommand(api: mockApi); |
| 186 | +``` |
| 187 | + |
| 188 | +## CI/CD & Release Workflow |
| 189 | + |
| 190 | +### PR Requirements |
| 191 | +- **Title**: Must follow Conventional Commits (enforced by `.github/workflows/pr.yml`) |
| 192 | +- **Checks**: All must pass - format, analyze, test |
| 193 | +- **Platforms**: Multi-platform builds run automatically (Linux, macOS, Windows) |
| 194 | + |
| 195 | +### Conventional Commits Format |
| 196 | +Examples: |
| 197 | +- `feat: add new command for X` |
| 198 | +- `fix: resolve issue with Y` |
| 199 | +- `chore: update dependencies` |
| 200 | +- `docs: update README` |
| 201 | + |
| 202 | +### Version Management |
| 203 | +**IMPORTANT**: Update BOTH files when releasing: |
| 204 | +1. `pubspec.yaml` - version field |
| 205 | +2. `bin/raygun_cli.dart` - version constant |
| 206 | + |
| 207 | +### Workflows |
| 208 | +- **pr.yml**: Validates PR title format |
| 209 | +- **main.yml**: Runs tests, format check, analysis, and builds binaries for all platforms |
| 210 | +- **release.yml**: On GitHub release, builds and uploads zipped binaries |
| 211 | + |
| 212 | +## Development Workflow |
| 213 | + |
| 214 | +### Local Testing |
| 215 | +```bash |
| 216 | +# Run CLI locally with arguments |
| 217 | +dart run bin/raygun_cli.dart <command> <args> |
| 218 | + |
| 219 | +# Use verbose flag for debug output |
| 220 | +dart run bin/raygun_cli.dart -v sourcemap --help |
| 221 | + |
| 222 | +# Set environment variables for testing |
| 223 | +export RAYGUN_APP_ID=test-app-id |
| 224 | +export RAYGUN_TOKEN=test-token |
| 225 | +export RAYGUN_API_KEY=test-api-key |
| 226 | +``` |
| 227 | + |
| 228 | +### Testing Workflow |
| 229 | +1. Write/modify API client code |
| 230 | +2. Add `@GenerateMocks([MyApi])` to test file |
| 231 | +3. Run `dart run build_runner build` to generate mocks |
| 232 | +4. Write tests using mock instances |
| 233 | +5. Run `dart test` to verify |
| 234 | + |
| 235 | +### Build for Distribution |
| 236 | +```bash |
| 237 | +# Compile for current platform |
| 238 | +dart compile exe bin/raygun_cli.dart -o raygun-cli |
| 239 | + |
| 240 | +# Note: Cross-compilation not supported; use CI for other platforms |
| 241 | +``` |
| 242 | + |
| 243 | +## Common Gotchas & Best Practices |
| 244 | + |
| 245 | +### Validation Order |
| 246 | +1. Always validate `--help` flag first before parsing other args |
| 247 | +2. `ConfigProp.load()` calls `exit(2)` if required config is missing |
| 248 | +3. Check mandatory args before executing business logic |
| 249 | + |
| 250 | +### File Operations |
| 251 | +- Use `File.existsSync()` before file operations |
| 252 | +- Throw descriptive exceptions if files don't exist |
| 253 | +- Use `.split("/").last` to get filename from path |
| 254 | + |
| 255 | +### Argument Parsing |
| 256 | +- Use `command.wasParsed('flag')` to check if flag was provided |
| 257 | +- Use `command['option']` to get option value |
| 258 | +- Use `command.command?.name` to get subcommand name |
| 259 | + |
| 260 | +### Async Patterns |
| 261 | +- Prefer `.then()` and `.catchError()` over try/catch for CLI commands |
| 262 | +- Always handle errors gracefully with user-friendly messages |
| 263 | +- Use `Future<bool>` for operations that can succeed or fail |
| 264 | + |
| 265 | +### String Conventions |
| 266 | +- Always use single quotes for strings (Dart convention) |
| 267 | +- Use string interpolation: `'Value: $variable'` or `'Value: ${expression}'` |
| 268 | + |
| 269 | +## Quick Reference |
| 270 | + |
| 271 | +### Command Examples |
| 272 | +```bash |
| 273 | +# Sourcemap upload (Flutter) |
| 274 | +dart run bin/raygun_cli.dart sourcemap -p flutter \ |
| 275 | + --uri=https://example.com/main.dart.js \ |
| 276 | + --app-id=XXX --token=YYY |
| 277 | + |
| 278 | +# Sourcemap upload (single file) |
| 279 | +dart run bin/raygun_cli.dart sourcemap \ |
| 280 | + --input-map=path/to/index.js.map \ |
| 281 | + --uri=https://example.com/index.js \ |
| 282 | + --app-id=XXX --token=YYY |
| 283 | + |
| 284 | +# Symbols upload |
| 285 | +dart run bin/raygun_cli.dart symbols upload \ |
| 286 | + --path=app.android-arm64.symbols \ |
| 287 | + --version=1.0.0 \ |
| 288 | + --app-id=XXX --token=YYY |
| 289 | + |
| 290 | +# Symbols list |
| 291 | +dart run bin/raygun_cli.dart symbols list \ |
| 292 | + --app-id=XXX --token=YYY |
| 293 | + |
| 294 | +# Symbols delete |
| 295 | +dart run bin/raygun_cli.dart symbols delete \ |
| 296 | + --id=2c7a3u3 \ |
| 297 | + --app-id=XXX --token=YYY |
| 298 | + |
| 299 | +# Deployments tracking |
| 300 | +dart run bin/raygun_cli.dart deployments \ |
| 301 | + --version=1.0.0 \ |
| 302 | + --token=YYY \ |
| 303 | + --api-key=ZZZ \ |
| 304 | + --scm-type=GitHub \ |
| 305 | + --scm-identifier=abc123 |
| 306 | + |
| 307 | +# Proguard upload |
| 308 | +dart run bin/raygun_cli.dart proguard \ |
| 309 | + --app-id=XXX \ |
| 310 | + --version=1.0.0 \ |
| 311 | + --path=mapping.txt \ |
| 312 | + --external-access-token=EAT \ |
| 313 | + --overwrite |
| 314 | + |
| 315 | +# iOS dSYM upload |
| 316 | +dart run bin/raygun_cli.dart dsym \ |
| 317 | + --app-id=XXX \ |
| 318 | + --path=path/to/dsym.zip \ |
| 319 | + --external-access-token=EAT |
| 320 | +``` |
| 321 | + |
| 322 | +### Environment Variables |
| 323 | +```bash |
| 324 | +export RAYGUN_APP_ID=your-app-id |
| 325 | +export RAYGUN_TOKEN=your-token |
| 326 | +export RAYGUN_API_KEY=your-api-key |
| 327 | +``` |
| 328 | + |
| 329 | +## Known TODOs & Future Improvements |
| 330 | +- Config file support (.raygun.yaml or similar) - see `lib/src/config_props.dart:9` |
| 331 | +- NodeJS sourcemap platform support (currently stubbed in sourcemap command) |
| 332 | +- System package manager installations (brew, apt, etc.) |
| 333 | + |
| 334 | +## Troubleshooting |
| 335 | + |
| 336 | +### Mock Generation Issues |
| 337 | +- Ensure `@GenerateMocks` annotation is present in test file |
| 338 | +- Run `dart pub get` to ensure dependencies are installed |
| 339 | +- Clean and rebuild: `dart run build_runner clean && dart run build_runner build` |
| 340 | + |
| 341 | +### Build Issues |
| 342 | +- Ensure Dart SDK version matches `pubspec.yaml` requirement (^3.5.0) |
| 343 | +- Run `dart pub get` to update dependencies |
| 344 | +- Check that version in `bin/raygun_cli.dart` matches `pubspec.yaml` |
| 345 | + |
| 346 | +### Test Failures |
| 347 | +- Verify mocks are regenerated after API changes |
| 348 | +- Check that mock behavior is properly stubbed with `when()` |
| 349 | +- Ensure async tests use `async/await` or return Future |
0 commit comments