Skip to content
Merged
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ Add entries under **Unreleased** in each pull request. The *Bump version* workfl

## [Unreleased]

### Added
- Crash reports include app and device details, heap use and uptime, the last 20 network calls (with errors and an excerpt of failed responses) and the last 50 `DeckLogger` entries, next to the breadcrumbs. Sizes are set with `CrashReporter.networkCallsInReport`, `logsInReport` and `stepsInReport`.
- `CrashReporter.addSection(title) { … }` adds your own context to every crash report.
- After a relaunch, an *App crashed last time* bar offers View, Share and dismiss, and the drawer header shows a *Crashed last time* chip (`CrashReporter.unseenCrash`, `CrashReporter.markSeen()`).
- On Android 11+, ANRs (with the system's thread dump) and native crashes are saved as crash reports on the next launch.
- `PreferenceFileRules` (`StoragePlugin(preferenceRules = …)`, `SharedPreferencesBrowser(rules = …)`) marks SharedPreferences files as app files, library files or hidden; testers can move a file between the lists from its page.
- `DeckHeaderChips.add(key) { … }` adds your own chips to the drawer header.
- `Pill` and `ConfirmDeleteButton` are public building blocks.

### Changed
- The default keyboard shortcut is now **Ctrl + Shift + K**. The Android emulator keeps Ctrl + Shift + D for its D-pad panel, so the old default never reached the app there.

### Fixed
- The *New version* chip no longer offers a build that isn't newer than the installed one (compared by version code).

## [1.2.0] - 2026-11-04

### Added
Expand Down
46 changes: 43 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,12 @@ OkHttpClient.Builder().apply { DebugTools.interceptors().forEach(::addIntercepto
| How | Default | Change it |
|---|---|---|
| Two-finger swipe toward the drawer, anywhere on screen | On | `gestures = DeckGestures(twoFingerSwipe = false)` |
| **Ctrl + Shift + D** (emulator or hardware keyboard) | On | `DeckGestures(keyboardShortcut = DeckShortcut(...))`, or `null` to turn off |
| **Ctrl + Shift + K** (emulator or hardware keyboard) | On | `DeckGestures(keyboardShortcut = DeckShortcut(...))`, or `null` to turn off |
| Floating bug button (drag it anywhere; it stays there) | On | `trigger = DeckTrigger.None` |
| Code | — | `DevDeck.open()`, `DevDeck.close()`, `DevDeck.toggle()` |

- The swipe is detected without taking touches, so taps, scrolls and system back keep working.
- On an emulator, a mouse can't do a two-finger swipe: use Ctrl + Shift + D or the floating button.
- On an emulator, a mouse can't do a two-finger swipe: use Ctrl + Shift + K or the floating button. Avoid Ctrl + Shift + letters the emulator keeps for its own panels (A–I, L–P, R–V).
- If your app uses two-finger horizontal pans (maps, canvases), turn the swipe off.
- To use your own icon on the floating button, pass an `ImageVector` or any composable:

Expand Down Expand Up @@ -168,7 +168,7 @@ DevDeck.install(
| `TimberLogsPlugin` | Logs | Every Timber call (`devdeck-timber`) | — |
| `LogsPlugin` | Logs | Your `DeckLogger.log(...)` entries, one row per tag | ✓ |
| `BreadcrumbsPlugin` | Logs | Screens, taps, requests and app events | ✓ |
| `CrashPlugin` | Logs | Saved crashes with stack traces | ✓ |
| `CrashPlugin` | Logs | Crash reports with the steps, network calls and logs before each crash; ANRs and native crashes on Android 11+ | ✓ |
| `AppActionsPlugin` | App | Deep links, WebView debugging, restart, clear cache or data, kill | ✓ |
| `UpdatesPlugin` | App | New test builds, release notes and install (`devdeck-appdistribution` for Firebase) | needs a source |
| `UiToolsPlugin` | User Interface | Slow animations, keylines, screenshots, long screenshots, recording, gallery | ✓ |
Expand Down Expand Up @@ -264,6 +264,22 @@ Log pages have level filters, search, an *All / By tag* view, pause, share and c

**Breadcrumbs** record what led up to a problem: screens, app events, taps (`Modifier.breadcrumb("Place order")` labels Compose controls), requests, and your own steps (`Breadcrumbs.add(...)`). The last 50 are saved with every crash and the last 100 go into bug reports.

### Crash reports

`CrashPlugin` saves a report when the app crashes, before the process dies. Each report has:

- the stack trace and thread;
- app version, device, Android version, heap use and how long the app had been running;
- the last 50 breadcrumbs, 20 network calls (method, URL, status, duration, errors and an excerpt of failed responses, with secrets already masked) and 50 `DeckLogger` entries;
- any sections your app adds.

After the relaunch, a small bar at the bottom says *App crashed last time*, with **View**, **Share** and dismiss, and the drawer header shows a *Crashed last time* chip. On Android 11+, ANRs (with the system's thread dump) and native crashes are picked up from the system on the next launch too.

```kotlin
CrashReporter.addSection("User") { "id=${session.userId}, plan=${session.plan}" } // runs while crashing: keep it quick
CrashReporter.networkCallsInReport = 30 // also logsInReport, stepsInReport
```

### Screen capture, bug reports and info

- **Screenshots and recordings:** under User Interface. While recording, a small floating bar shows the time with pause and stop, so the drawer can stay closed. DevDeck's own floating button and badges are left out of screenshots.
Expand Down Expand Up @@ -293,9 +309,33 @@ val cachePlugin = deckPlugin(id = "cache", section = "System 📱") {
| `SegmentedPills`, `FilterPill`, `FilterPillRow`, `ActionPill`, `ActionPillRow`, `SearchField` | View switchers, filters and search |
| `BulletItem`, `InfoGroup`, `KeyValueItem`, `CodeBlock`, `TextItem` | Showing information |

| `Pill`, `ConfirmDeleteButton` | Tags like the network method and status, and a delete icon that asks for a second tap |

- Close or navigate from a plugin with `LocalDeckState.current?.close()` or `?.navigateTo(title) { … }`.
- For setup work, implement `DeckPlugin` and override `onAttach(context)`.

### Header chips, crash report sections and preference files

```kotlin
// A chip under the drawer title; return null while there is nothing to show.
DeckHeaderChips.add("environment") { if (isStaging) DeckHeaderChip("Staging", Color(0xFFF57C00)) else null }

// Extra context in every crash report.
CrashReporter.addSection("Experiment") { experiments.activeNames().joinToString() }

// Which SharedPreferences files are the app's, which are library files and which are never listed.
// Names are exact, or a prefix ending in *.
StoragePlugin(
preferenceRules = PreferenceFileRules(
app = setOf("com.google.maps.cache"), // looks like a library file, but it's yours
library = setOf("analytics_*"), // listed only after "Show library files"
hidden = setOf("secure_session"), // never listed
),
)
```

Testers can also move a file between the app and library lists from its page (*Move to library files* / *Move to app files*); that choice is kept on the device.

### Customizing

```kotlin
Expand Down
70 changes: 66 additions & 4 deletions devdeck/api/devdeck.api
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ public final class io/devdeck/DeckGestures$Companion {
public final fun getDisabled ()Lio/devdeck/DeckGestures;
}

public final class io/devdeck/DeckHeaderChip {
public static final field $stable I
public synthetic fun <init> (Ljava/lang/String;Landroidx/compose/ui/graphics/Color;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (Ljava/lang/String;Landroidx/compose/ui/graphics/Color;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getColor-QN2ZGVo ()Landroidx/compose/ui/graphics/Color;
public final fun getOnClick ()Lkotlin/jvm/functions/Function0;
public final fun getText ()Ljava/lang/String;
}

public final class io/devdeck/DeckHeaderChips {
public static final field $stable I
public static final field INSTANCE Lio/devdeck/DeckHeaderChips;
public final fun add (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)V
public final fun remove (Ljava/lang/String;)V
}

public final class io/devdeck/DeckPage {
public static final field $stable I
public final fun getTitle ()Ljava/lang/String;
Expand Down Expand Up @@ -416,6 +432,10 @@ public final class io/devdeck/components/ConfirmButtonItemKt {
public static final fun ConfirmButtonItem (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;Ljava/lang/String;Ljava/lang/String;ZLandroidx/compose/runtime/Composer;II)V
}

public final class io/devdeck/components/ConfirmDeleteButtonKt {
public static final fun ConfirmDeleteButton (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZLjava/lang/String;Landroidx/compose/runtime/Composer;II)V
}

public final class io/devdeck/components/DeckCardKt {
public static final fun DeckCard (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}
Expand Down Expand Up @@ -461,6 +481,10 @@ public final class io/devdeck/components/PageLinkItemKt {
public static final fun PageLinkItem (Ljava/lang/String;Landroidx/compose/ui/Modifier;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V
}

public final class io/devdeck/components/PillKt {
public static final fun Pill-sW7UJKQ (Ljava/lang/String;JLandroidx/compose/ui/Modifier;ZLandroidx/compose/runtime/Composer;II)V
}

public final class io/devdeck/components/SearchFieldKt {
public static final fun SearchField (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)V
}
Expand Down Expand Up @@ -574,18 +598,25 @@ public final class io/devdeck/crash/CrashListKt {

public final class io/devdeck/crash/CrashRecord {
public static final field $stable I
public fun <init> (Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public synthetic fun <init> (Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V
public synthetic fun <init> (Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/io/File;
public final fun component2 ()J
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun component5 ()Ljava/lang/String;
public final fun component6 ()Ljava/lang/String;
public final fun copy (Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/devdeck/crash/CrashRecord;
public final fun component7 ()Ljava/util/List;
public final synthetic fun copy (Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/devdeck/crash/CrashRecord;
public final fun copy (Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)Lio/devdeck/crash/CrashRecord;
public static synthetic fun copy$default (Lio/devdeck/crash/CrashRecord;Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/devdeck/crash/CrashRecord;
public static synthetic fun copy$default (Lio/devdeck/crash/CrashRecord;Ljava/io/File;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;ILjava/lang/Object;)Lio/devdeck/crash/CrashRecord;
public fun equals (Ljava/lang/Object;)Z
public final fun getFile ()Ljava/io/File;
public final fun getReport ()Ljava/lang/String;
public final fun getSections ()Ljava/util/List;
public final fun getStackTrace ()Ljava/lang/String;
public final fun getSteps ()Ljava/lang/String;
public final fun getThread ()Ljava/lang/String;
Expand All @@ -598,11 +629,21 @@ public final class io/devdeck/crash/CrashRecord {
public final class io/devdeck/crash/CrashReporter {
public static final field $stable I
public static final field INSTANCE Lio/devdeck/crash/CrashReporter;
public final fun addSection (Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V
public final fun clear ()V
public final fun crashes ()Ljava/util/List;
public final fun getLogsInReport ()I
public final fun getMaxCrashes ()I
public final fun getNetworkCallsInReport ()I
public final fun getStepsInReport ()I
public final fun getUnseenCrash ()Lio/devdeck/crash/CrashRecord;
public final fun install (Landroid/content/Context;)V
public final fun markSeen ()V
public final fun removeSection (Ljava/lang/String;)V
public final fun setLogsInReport (I)V
public final fun setMaxCrashes (I)V
public final fun setNetworkCallsInReport (I)V
public final fun setStepsInReport (I)V
}

public final class io/devdeck/device/AppInfoKt {
Expand Down Expand Up @@ -1183,8 +1224,10 @@ public final class io/devdeck/plugin/PerformancePlugin : io/devdeck/plugin/DeckP
public final class io/devdeck/plugin/StoragePlugin : io/devdeck/plugin/DeckPlugin {
public static final field $stable I
public fun <init> ()V
public fun <init> (ZZLkotlin/jvm/functions/Function1;Ljava/lang/String;)V
public synthetic fun <init> (ZZLkotlin/jvm/functions/Function1;Ljava/lang/String;)V
public synthetic fun <init> (ZZLkotlin/jvm/functions/Function1;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun <init> (ZZLkotlin/jvm/functions/Function1;Ljava/lang/String;Lio/devdeck/storage/PreferenceFileRules;)V
public synthetic fun <init> (ZZLkotlin/jvm/functions/Function1;Ljava/lang/String;Lio/devdeck/storage/PreferenceFileRules;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun Content (Landroidx/compose/runtime/Composer;I)V
public fun getId ()Ljava/lang/String;
public fun getSection ()Ljava/lang/String;
Expand Down Expand Up @@ -1342,8 +1385,27 @@ public final class io/devdeck/simulation/NetworkSimulatorSettingsKt {
public static final fun NetworkSimulatorSettings (Landroidx/compose/ui/Modifier;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)V
}

public final class io/devdeck/storage/PreferenceFileRules {
public static final field $stable I
public fun <init> ()V
public fun <init> (Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;)V
public synthetic fun <init> (Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/util/Set;
public final fun component2 ()Ljava/util/Set;
public final fun component3 ()Ljava/util/Set;
public final fun copy (Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;)Lio/devdeck/storage/PreferenceFileRules;
public static synthetic fun copy$default (Lio/devdeck/storage/PreferenceFileRules;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ILjava/lang/Object;)Lio/devdeck/storage/PreferenceFileRules;
public fun equals (Ljava/lang/Object;)Z
public final fun getApp ()Ljava/util/Set;
public final fun getHidden ()Ljava/util/Set;
public final fun getLibrary ()Ljava/util/Set;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/devdeck/storage/SharedPreferencesBrowserKt {
public static final fun SharedPreferencesBrowser (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
public static final synthetic fun SharedPreferencesBrowser (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
public static final fun SharedPreferencesBrowser (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lio/devdeck/storage/PreferenceFileRules;Landroidx/compose/runtime/Composer;II)V
public static final fun SharedPreferencesFile (Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}

Expand Down
36 changes: 36 additions & 0 deletions devdeck/src/main/kotlin/io/devdeck/DeckHeaderChips.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.devdeck

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.ui.graphics.Color

/** A chip in the drawer header, under the title. [color] tints it; null draws a neutral outlined chip. */
@Immutable
public class DeckHeaderChip(public val text: String, public val color: Color? = null, public val onClick: (() -> Unit)? = null)

/**
* Chips the app adds to the drawer header, next to DevDeck's own (recording, new version, simulations). Each chip is
* a composable that returns null while it has nothing to show, so it can follow Compose state:
*
* ```
* DeckHeaderChips.add("environment") { if (isStaging) DeckHeaderChip("Staging", Color(0xFFF57C00)) else null }
* ```
*/
public object DeckHeaderChips {

private val entries = mutableStateListOf<Pair<String, @Composable () -> DeckHeaderChip?>>()

/** Adds a chip, or replaces the one already added under [key]. */
public fun add(key: String, chip: @Composable () -> DeckHeaderChip?) {
val index = entries.indexOfFirst { it.first == key }
if (index >= 0) entries[index] = key to chip else entries += key to chip
}

public fun remove(key: String) {
entries.removeAll { it.first == key }
}

@Composable
internal fun current(): List<DeckHeaderChip> = entries.toList().mapNotNull { (_, chip) -> chip() }
}
Loading
Loading