diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..b2bdf4c10 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,174 @@ +# Chucker - Android HTTP Inspector + +Chucker is an in-app HTTP inspector for Android. It intercepts OkHttp traffic, stores it in a Room database, and provides a built-in UI to browse requests/responses. It ships as two variants: `library` (full, for debug) and `library-no-op` (empty stubs, for release). + +## Modules + +| Module | What it is | When to touch it | +|--------|-----------|-----------------| +| `library` | Full interceptor + UI + database | Adding/changing HTTP inspection features | +| `library-no-op` | Empty stubs matching library's public API | **Must update whenever library's public API changes** | +| `sample` | Demo app exercising all features | Testing changes, adding demo for new features | + +## Quick Start + +```bash +# Build everything +./gradlew build + +# Run tests (library only has tests) +./gradlew :library:test + +# Run all quality checks (do this before pushing) +./gradlew lint ktlintCheck detekt apiCheck + +# Auto-fix formatting +./gradlew ktlintFormat + +# Install sample app to test changes +./gradlew :sample:installDebug + +# Install git hooks (trufflehog + cac-validate + yaakhook) +./gradlew installGitHook +``` + +## How to Add a New Feature + +### Adding a new interceptor option (e.g., new config flag) + +1. **Add to `ChuckerInterceptor.Builder`** in `library/src/main/kotlin/.../api/ChuckerInterceptor.kt` + - Add an `internal var` field on the Builder + - Add a `public fun` builder method that sets it and returns `this` + - Use it in the private constructor or pass it to processors + +2. **Mirror in no-op** in `library-no-op/src/main/kotlin/.../api/ChuckerInterceptor.kt` + - Add the same builder method signature, but make it a no-op (just return `this`) + +3. **Update binary API files** — run `./gradlew apiDump` to regenerate `library/api/library.api` and `library-no-op/api/library-no-op.api` + +4. **Add tests** in `library/src/test/kotlin/.../api/ChuckerInterceptorTest.kt` + +5. **Demo it** in `sample/.../OkHttpUtils.kt` where the interceptor is built + +### Adding a new data field to HTTP transactions + +1. **Add field** to `HttpTransaction` entity in `library/.../internal/data/entity/HttpTransaction.kt` + - Must have `@ColumnInfo(name = "fieldName")` annotation + - **Increment database version** in `ChuckerDatabase.kt` (currently version 7) + - Data will be wiped on upgrade (destructive migration is enabled — this is fine for a debug lib) + +2. **Populate it** in `RequestProcessor` or `ResponseProcessor` in `library/.../internal/support/` + +3. **Display it** in the relevant UI fragment (`TransactionOverviewFragment`, `TransactionPayloadFragment`) + +4. **ProGuard**: `HttpTransaction` is already kept in `proguard-rules.pro`, so new fields are safe + +### Adding a new UI screen + +1. Create Activity extending `BaseChuckerActivity` in `library/.../internal/ui/` +2. Create ViewModel extending `ViewModel` with LiveData from the repository +3. Use ViewBinding (enabled in build config) +4. Add to `AndroidManifest.xml` — use `android:exported="false"` for internal screens +5. All resource names must start with `chucker_` prefix (enforced by Gradle) + +### Adding a new public API class + +1. Create in `library/.../api/` package with `public` visibility +2. Use Builder pattern if it needs configuration +3. Create matching no-op stub in `library-no-op/.../api/` +4. Run `./gradlew apiDump` to update `.api` tracking files +5. Add tests + +## Code Conventions + +### Visibility (STRICT — compiler enforced) +```kotlin +// Every public member MUST have explicit visibility modifier +public class MyClass { // explicit public required + public fun doThing() { } // explicit public required + internal fun helper() { } // internal for library-internal use + private val cache = mutableMapOf() +} +``` +The `-Xexplicit-api=strict` flag means omitting `public`/`internal`/`private` is a **compile error**. + +### Patterns used in this codebase +- **Builder pattern** for public configurable classes (ChuckerInterceptor, ChuckerCollector) +- **Repository pattern** for data access (HttpTransactionRepository → Room DAO) +- **MVVM** for UI (ViewModel + LiveData + ViewBinding) +- **Processor pattern** for request/response handling (RequestProcessor, ResponseProcessor) +- **Null Object pattern** for no-op variant (same API, empty bodies) + +### Naming +- Resources: `chucker_` prefix (e.g., `chucker_ic_launcher`, `chucker_main_activity`) +- Test methods: backtick sentences (e.g., `` `image response body is available to Chucker` ``) +- Constants: `private companion object { private const val MAX_CONTENT_LENGTH = 250_000L }` + +### Testing +- JUnit 5 (Jupiter) — use `@Test`, `@ParameterizedTest`, `@ExtendWith` +- MockK for mocking — `mockk()` +- Truth for assertions — `assertThat(result).isEqualTo(expected)` +- MockWebServer for HTTP — set up server, enqueue responses, assert intercepted data +- Test utilities in `library/src/test/.../util/` (TestTransactionFactory, ClientFactory, etc.) + +## Things That Will Break Your PR + +| Check | Command | Common failure | +|-------|---------|---------------| +| **Detekt** | `./gradlew detekt` | Method complexity >16, condition complexity >5, >5 return statements | +| **KtLint** | `./gradlew ktlintCheck` | Formatting issues (fix with `./gradlew ktlintFormat`) | +| **Lint** | `./gradlew lint` | Warnings treated as errors (except RtlEnabled, GradleDependency) | +| **API Check** | `./gradlew apiCheck` | Public API changed without updating `.api` files (fix with `./gradlew apiDump`) | +| **Tests** | `./gradlew :library:test` | Test failures | +| **Pre-commit hooks** | Auto on commit | TruffleHog finds secrets, CAC validation fails | + +## Critical Gotchas + +1. **Two libraries must stay in sync** — Any public API change in `library` must be mirrored in `library-no-op`. The no-op has empty implementations but identical method signatures. If they diverge, apps using the debug/release split pattern will fail to compile. + +2. **Database is destructive** — Room uses `fallbackToDestructiveMigration()`. Schema changes wipe all data. No migration files exist. This is intentional for a debug tool. + +3. **ProGuard keeps only HttpTransaction** — Only `HttpTransaction` is in `proguard-rules.pro`. If you add new entities used by Room or Gson, add them too. + +4. **`maxIssues: 1` in detekt** — Even 2 detekt violations will fail the build. Fix issues, don't suppress. + +5. **Version comes from git** — At build time, version is the current git tag (if tagged) or `branchname-SNAPSHOT`. Shallow clones may not detect tags correctly. + +6. **Resource prefix is enforced** — All layout, string, drawable resources must start with `chucker_`. The build will fail otherwise. + +## CI/CD Pipeline + +| Workflow | Trigger | What it does | +|----------|---------|-------------| +| `pre-merge.yaml` | PRs + push to develop | Tests, lint, detekt, ktlint, apiCheck | +| `publish-snapshot.yaml` | Push to develop | Publishes `SNAPSHOT` to Sonatype | +| `publish-release.yaml` | Push git tag | Publishes release to Sonatype staging | +| `close-and-release-repository.yaml` | Manual dispatch | Promotes staging to Maven Central | +| `gradle-wrapper-validation.yml` | PRs + push to develop | Validates Gradle wrapper integrity | + +### How to publish a release +1. Update `VERSION_NAME` in `gradle.properties` (e.g., `4.0.0`) +2. Commit & tag: `git tag 4.0.0 && git push origin 4.0.0` +3. `publish-release.yaml` auto-publishes to Sonatype staging +4. Manually trigger `close-and-release-repository.yaml` to push to Maven Central +5. Bump back to next snapshot: `VERSION_NAME=4.1.0-SNAPSHOT` + +### Publishing to JFrog Artifactory (Meesho internal) +```bash +./gradlew artifactoryPublish +``` +Publishes as `com.meesho.android.chucker:library` / `com.meesho.android.chucker:library-no-op`. + +## Tech Stack + +| | | +|---|---| +| **Language** | Kotlin 1.9.23, Java 17 target | +| **SDK** | minSdk 21, targetSdk 35, compileSdk 35 | +| **AGP** | 8.9.1 | +| **HTTP** | OkHttp 4.9.0 | +| **Database** | Room 2.6.1 | +| **Serialization** | Gson 2.9.0 | +| **Async** | Kotlin Coroutines 1.7.3 + LiveData | +| **UI** | AppCompat + Material 1.2.1 + ViewBinding | +| **Testing** | JUnit 5, MockK 1.10.2, Robolectric 4.4, Truth 1.1 | diff --git a/library-no-op/CLAUDE.md b/library-no-op/CLAUDE.md new file mode 100644 index 000000000..383e29e17 --- /dev/null +++ b/library-no-op/CLAUDE.md @@ -0,0 +1,53 @@ +# library-no-op - Release Build Stubs + +Zero-overhead stubs for production/release builds. Same public API as `library`, but every method is empty or passes through. Apps use this pattern: + +```gradle +debugImplementation project(':library') // Full Chucker in debug +releaseImplementation project(':library-no-op') // Empty stubs in release +``` + +## What's Here + +Five files in `src/main/kotlin/.../api/`, each mirroring the main library's public API: + +| Class | What the no-op does | +|-------|-------------------| +| `ChuckerInterceptor` | `intercept()` just calls `chain.proceed(request)` — passthrough. Builder methods return `this`. | +| `Chucker` | `isOp = false`. `getLaunchIntent()` returns empty `Intent()`. | +| `ChuckerCollector` | Constructor accepts same params, stores nothing. | +| `RetentionManager` | `doMaintenance()` is empty. | +| `BodyDecoder` | Interface only (same as main library). | + +**Dependencies:** Only OkHttp + Kotlin stdlib. No Room, no AndroidX, no Material, no Coroutines. + +## When You Need to Touch This + +**Every time you change the public API in `library`:** + +1. Add/remove/modify the same method signature here +2. Implementation should be empty (return `this`, return empty value, or do nothing) +3. Run `./gradlew apiDump` to update `api/library-no-op.api` +4. Run `./gradlew apiCheck` to verify compatibility + +### Example: Adding a new Builder method + +In `library/`: +```kotlin +public fun myNewOption(value: Boolean): Builder = apply { + this.myNewOption = value +} +``` + +In `library-no-op/`: +```kotlin +@Suppress("UnusedPrivateMember") +public fun myNewOption(value: Boolean): Builder = this +``` + +## Gotchas + +- **API files may show type erasure differences** — the no-op `.api` file may show `Object` where the main library shows specific types. This is a known quirk of the binary compatibility validator with the no-op's simplified generics. +- **Don't add dependencies** — the whole point is zero overhead. No Room, no UI, no nothing. +- **Same namespace** — both modules use `com.chuckerteam.chucker` package. They're mutually exclusive at build time (debug vs release). +- **No tests** — stubs are trivial. If a stub is complex enough to need tests, it's doing too much. \ No newline at end of file diff --git a/library/CLAUDE.md b/library/CLAUDE.md new file mode 100644 index 000000000..6d2dbbee6 --- /dev/null +++ b/library/CLAUDE.md @@ -0,0 +1,120 @@ +# library - Chucker Core Library + +The main module. Contains the OkHttp interceptor, Room database, and Android UI for HTTP inspection. + +## Architecture + +``` +api/ # Public API — what app developers use + ChuckerInterceptor # OkHttp interceptor (Builder pattern) + ChuckerCollector # Data collection lifecycle + Chucker # Utility singleton (launch intent, notifications) + RetentionManager # Data cleanup policy + BodyDecoder # Interface for custom decoders (e.g., Protobuf) + +internal/data/ # Persistence layer + entity/ # HttpTransaction (Room entity, 60+ fields) + room/ # ChuckerDatabase (v7, destructive migration) + repository/ # HttpTransactionRepository (LiveData queries) + har/ # HAR 1.2 export format classes + +internal/support/ # Processing & utilities + RequestProcessor # Extracts request metadata + payload + ResponseProcessor # Extracts response metadata + payload (multicast via TeeSource) + NotificationHelper # Shows persistent notification with transaction count + *Sharable classes # Export as text/curl/HAR file + Stream utilities # TeeSource, LimitingSource, DepletingSource, ReportingSink + +internal/ui/ # Android MVVM UI + MainActivity # Transaction list (singleTask, separate task affinity) + TransactionActivity # Detail view: Overview + Request + Response tabs + *ViewModel classes # LiveData from repository, filtering by code/path +``` + +## How to Build & Test + +```bash +./gradlew :library:test # Run all tests (JUnit 5 + Robolectric) +./gradlew :library:lint # Lint (warnings = errors) +./gradlew :library:assembleDebug # Build AAR +``` + +## Data Flow + +``` +App makes HTTP call via OkHttp + → ChuckerInterceptor.intercept() + → RequestProcessor extracts headers, URL, body (with size limit + custom decoders) + → ChuckerCollector.onRequestSent() → Room DB insert + → chain.proceed(request) [actual network call] + → ResponseProcessor extracts status, headers, body (TeeSource for multicast) + → ChuckerCollector.onResponseReceived() → Room DB update + notification + → UI observes LiveData from Room → RecyclerView updates automatically +``` + +## How to Make Changes + +### Adding a new field to display in the transaction detail screen + +1. If it's a new data field, add to `HttpTransaction` entity with `@ColumnInfo` +2. Increment DB version in `ChuckerDatabase` (currently 7) — data wipes on upgrade, that's fine +3. Populate in `RequestProcessor` or `ResponseProcessor` +4. Add UI in the relevant fragment: + - `TransactionOverviewFragment` — metadata (URL, status, timing) + - `TransactionPayloadFragment` — headers and body content +5. If it's a list field, add adapter item type in `TransactionPayloadAdapter` + +### Adding a new export format + +1. Create class implementing `Sharable` interface in `internal/support/` +2. Add menu option in `TransactionActivity` menu +3. Handle in `TransactionActivity.onOptionsItemSelected()` + +### Adding a new public API option + +1. Add Builder field + method in `ChuckerInterceptor` (return `this` for chaining) +2. Pass to processor via constructor +3. **Must mirror in library-no-op** — same method signature, empty body +4. Run `./gradlew apiDump` to update `.api` files +5. Add test in `ChuckerInterceptorTest` + +### Modifying the Room database + +- Entity: `HttpTransaction` in `internal/data/entity/` +- DAO: `HttpTransactionDao` in `internal/data/room/` +- Database: `ChuckerDatabase` — uses `fallbackToDestructiveMigration()` (no migration files needed) +- All fields need `@ColumnInfo` annotation +- `HttpTransaction` is kept by ProGuard (`proguard-rules.pro`) — new fields are safe + +## Testing Patterns + +```kotlin +// Typical interceptor test structure +@ExtendWith(NoLoggerRule::class) +internal class ChuckerInterceptorTest { + @get:Rule val server = MockWebServer() + + @Test + fun `descriptive test name in backticks`() { + // Use ClientFactory to create OkHttp client with Chucker + // Use ChuckerInterceptorDelegate to wrap and assert + // Use TestTransactionFactory for mock data + // Assert with Truth: assertThat(result).isEqualTo(expected) + } +} +``` + +Key test utilities (in `src/test/.../util/`): +- `TestTransactionFactory` — creates mock HttpTransaction objects +- `ClientFactory` — OkHttp client setup variants +- `ChuckerInterceptorDelegate` — interceptor wrapper for assertions +- `NoLoggerRule` — suppresses Chucker logging in tests + +## Gotchas + +- **Explicit API is strict** — every `public`/`internal`/`private` must be written out. Omitting it = compile error. +- **Resource prefix** — all resources must start with `chucker_`. Build fails otherwise. +- **Detekt limits** — max 16 method complexity, max 5 condition complexity, max 5 returns. `maxIssues: 1`. +- **`MainScope()` in ChuckerCollector is never cancelled** — create one collector and reuse it. +- **Manifest: MainActivity uses `singleTask` + separate task affinity** — this enables multi-window but may conflict with host app's task setup. +- **No RTL support** — lint check is disabled, UI is LTR only. \ No newline at end of file diff --git a/sample/CLAUDE.md b/sample/CLAUDE.md new file mode 100644 index 000000000..98c8122b7 --- /dev/null +++ b/sample/CLAUDE.md @@ -0,0 +1,62 @@ +# sample - Demo Application + +Interactive Android app that exercises all Chucker features. Use it to test your changes visually. + +## How to Run + +```bash +./gradlew :sample:installDebug # Full Chucker UI (debug) +./gradlew :sample:installRelease # No-op Chucker (release, to verify no-op works) +``` + +## What the App Does + +Two buttons: +- **"Do HTTP activity"** — Fires 20+ HTTP requests covering every scenario, then open Chucker notification to inspect them +- **"Launch Chucker directly"** — Opens the Chucker UI (debug only) + +Radio buttons switch between Application interceptor and Network interceptor modes. + +### HTTP scenarios covered + +| Task Class | What it tests | +|-----------|--------------| +| `HttpBinHttpTask` | GET, POST, PUT, PATCH, DELETE, redirects, auth, gzip/brotli/deflate, status codes (201/401/500), streaming | +| `DummyImageHttpTask` | Image response bodies (PNG downloads) | +| `PostmanEchoHttpTask` | Large JSON payloads, Protocol Buffer encoding/decoding | + +## Key Files + +| File | Purpose | +|------|---------| +| `MainActivity.kt` | UI entry point, triggers HTTP tasks | +| `OkHttpUtils.kt` | **Builds the OkHttpClient with ChuckerInterceptor** — this is the reference for how to integrate Chucker | +| `HttpBinHttpTask.kt` | Most comprehensive HTTP test (20+ varied requests) | +| `PokemonProtoBodyDecoder.kt` | Custom `BodyDecoder` example for Protocol Buffers | +| `LargeJson.kt` | Pre-built large JSON payload for testing truncation | +| `InterceptorType.kt` | Enum + provider for switching Application/Network interceptor | + +## How to Use for Testing Your Changes + +1. Make your change in `library/` +2. Run `./gradlew :sample:installDebug` +3. Tap "Do HTTP activity" — this fires diverse requests through Chucker +4. Open Chucker from the notification or "Launch Chucker directly" button +5. Verify your change works in the UI + +### Adding a demo for a new feature + +1. If it's a new interceptor option: configure it in `OkHttpUtils.kt` where the interceptor is built +2. If it needs new HTTP requests: create a new `HttpTask` implementation or add to `HttpBinHttpTask` +3. If it needs a UI trigger: add a button in `activity_main_sample.xml` and wire it in `MainActivity` + +## Build Variants + +- **Debug**: Full Chucker library, StrictMode enabled, LeakCanary for memory leak detection, cleartext HTTP allowed (network security config) +- **Release**: No-op library, no debug tools + +## Gotchas + +- **Uses external APIs** (httpbin.org, dummyimage.com, postman-echo.com) — tests fail if these services are down. This is a network-dependent demo, not a unit test. +- **ProGuard is disabled** (`minifyEnabled false`) — this doesn't test minification. If you need to verify ProGuard rules, test with a separate app that has minification enabled. +- **Wire plugin** generates Protobuf classes at build time — if the build seems slow or fails on proto files, check `sample/src/main/proto/`. \ No newline at end of file