Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 174 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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<String, String>()
}
```
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<ChuckerCollector>()`
- 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 |
53 changes: 53 additions & 0 deletions library-no-op/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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.
120 changes: 120 additions & 0 deletions library/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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
```
Comment on lines +7 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor | ⚑ Quick win

Add language specifier to code block for linter compliance.

The ASCII art architecture diagram should specify a language identifier (e.g., text or plaintext) to satisfy markdownlint MD040 rule.

πŸ“ Proposed fix
-```
+```text
 api/                    # Public API β€” what app developers use
   ChuckerInterceptor    #   OkHttp interceptor (Builder pattern)
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
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
```
🧰 Tools
πŸͺ› markdownlint-cli2 (0.22.1)

[warning] 7-7: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/CLAUDE.md` around lines 7 - 32, The markdown block containing the
ASCII architecture diagram (the fenced code block that starts with ``` and lists
entries like ChuckerInterceptor, ChuckerCollector, RetentionManager,
BodyDecoder, RequestProcessor, ResponseProcessor, MainActivity,
TransactionActivity) is missing a language tag and triggers markdownlint MD040;
update the fenced code block opening from ``` to ```text (or ```plaintext) so
the linter recognizes it as a plain text block and the diagram lines (e.g.,
ChuckerInterceptor, ChuckerCollector, RequestProcessor, ResponseProcessor,
MainActivity, TransactionActivity) remain unchanged.


## 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
```
Comment thread
mohitguptameesho marked this conversation as resolved.

## 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.
Loading
Loading