|
| 1 | +--- |
| 2 | +name: reprexes |
| 3 | +description: > |
| 4 | + Isolate a technical problem into a minimal reproducible example ("reprex") |
| 5 | + and iterate fixes on that instead of inside the full application. Use when |
| 6 | + debugging a bug whose cause isn't obvious after a first look, when a failure |
| 7 | + only surfaces deep in a large pipeline / app / render, when the full-context |
| 8 | + test loop is slow, or before filing an upstream issue. Invoke explicitly |
| 9 | + with /reprexes. |
| 10 | +user-invocable: true |
| 11 | +allowed-tools: |
| 12 | + - Bash |
| 13 | + - Read |
| 14 | + - Write |
| 15 | + - Edit |
| 16 | +--- |
| 17 | + |
| 18 | +# reprexes |
| 19 | + |
| 20 | +When a technical problem is hard to pin down, **don't debug it in the full |
| 21 | +context of the motivating application.** Extract a minimal, self-contained |
| 22 | +reproduction of the phenomenon, iterate candidate fixes on *that* (a fast, |
| 23 | +clean loop), then port the working fix back to the real code. |
| 24 | + |
| 25 | +Reference: <https://r4ds.hadley.nz/workflow-help.html#making-a-reprex>. |
| 26 | +The payoff is real: often, the act of building a thorough reprex surfaces the |
| 27 | +cause on its own — the noise you strip away was hiding it. |
| 28 | + |
| 29 | +## When this fires |
| 30 | + |
| 31 | +- A bug whose cause isn't obvious after a first read of the code. |
| 32 | +- A failure that only shows up deep inside a large pipeline, app, or a full |
| 33 | + `quarto render` — where each reproduction attempt is slow. |
| 34 | +- Iterating on a fix that's expensive to test in the full context. |
| 35 | +- Before asking a human for help or filing an upstream/package issue — a |
| 36 | + reprex is what they'll ask for anyway. |
| 37 | + |
| 38 | +Don't bother for a one-line obvious fix, or a problem you can already see and |
| 39 | +test cheaply in place. |
| 40 | + |
| 41 | +## The two requirements |
| 42 | + |
| 43 | +A reprex is only useful if it is BOTH: |
| 44 | + |
| 45 | +1. **Reproducible** — it captures *everything* needed to trigger the |
| 46 | + phenomenon: every `library()` call, and code that creates every object it |
| 47 | + uses. Someone (or a fresh session) can run it cold and see the same thing. |
| 48 | +2. **Minimal** — everything unrelated to the phenomenon is stripped out: |
| 49 | + small or built-in data instead of the real dataset, only the lines that |
| 50 | + matter. |
| 51 | + |
| 52 | +The tension between these two is the whole game: include enough to reproduce, |
| 53 | +but nothing more. |
| 54 | + |
| 55 | +## Procedure |
| 56 | + |
| 57 | +1. **Hypothesize the minimal trigger.** What is the smallest combination of |
| 58 | + data + operation you believe causes the phenomenon? |
| 59 | +2. **Create a standalone scratch file** outside the repo tree (e.g. |
| 60 | + `/tmp/reprex.R`, or a tiny `/tmp/reprex.qmd` for a render bug; on a |
| 61 | + non-Unix machine use `tempfile(fileext = ".R")` / `tempdir()` for a |
| 62 | + portable path). Put in it, in order: |
| 63 | + - the package loads (`library(...)`), |
| 64 | + - the minimal data (see tactics below), |
| 65 | + - the minimal code that triggers the phenomenon, with a comment marking |
| 66 | + where it goes wrong. |
| 67 | +3. **Run it in a clean session** and confirm it reproduces. For R, run with |
| 68 | + `Rscript /tmp/reprex.R` (a fresh process — no stale `globalenv()` state |
| 69 | + masking or faking the bug). For a Quarto page, render just that file: |
| 70 | + `quarto render /tmp/reprex.qmd --to html`, not the whole site. |
| 71 | +4. **Minimize.** Remove pieces until the phenomenon disappears — the last |
| 72 | + removal that "fixes" it implicates that piece. (Or build up from nothing |
| 73 | + until it appears.) Keep the data as small as it can be while still failing. |
| 74 | +5. **Iterate fixes on the reprex**, not the full app. This is the fast loop |
| 75 | + the whole technique exists to create. |
| 76 | +6. **Port the fix back** to the real code and verify it there. |
| 77 | +7. **Clean up.** Delete the scratch file (it lives in `/tmp`, so it never |
| 78 | + touches the repo). If the bug was subtle, consider promoting the reprex |
| 79 | + into a real regression test (`testthat`) instead of discarding it. |
| 80 | + |
| 81 | +## Minimizing the data |
| 82 | + |
| 83 | +- Prefer a **built-in dataset** (`mtcars`, `mpg`) or a hand-built |
| 84 | + tiny frame over the real data. |
| 85 | +- If you must use a slice of real data, serialize the minimal slice with |
| 86 | + `dput()` so the reprex recreates it inline — no external file dependency. |
| 87 | +- Shrink to the fewest rows/columns that still show the phenomenon. |
| 88 | + |
| 89 | +## R / Quarto specifics |
| 90 | + |
| 91 | +- In R packages and Quarto projects, reprexes are usually short R snippets or |
| 92 | + a single standalone `.qmd` page. Respect the repo's lint config if the |
| 93 | + reprex code will be ported back. |
| 94 | +- The **`reprex` package** (tidyverse, <https://reprex.tidyverse.org/>) |
| 95 | + formats a reprex for sharing: it runs your code in a clean, separate R |
| 96 | + session (via `callr` since reprex 2.0) and emits code **plus actual |
| 97 | + output**. |
| 98 | + Copy the code and call `reprex::reprex()` (reads the clipboard by default), |
| 99 | + or point it at a file with `reprex(input = "/tmp/reprex.R")` (or a |
| 100 | + `tempfile(fileext = ".R")` path on non-Unix machines) — handy from a |
| 101 | + non-interactive CLI session where there's no clipboard. Use it when the |
| 102 | + output is destined for a PR comment or an upstream issue. Useful arguments: |
| 103 | + - `venue =` — output format: |
| 104 | + - `"gh"` — GitHub-flavored Markdown (default) |
| 105 | + - `"so"` / `"ds"` — Stack Overflow / Discourse |
| 106 | + - `"slack"` — Slack message |
| 107 | + - `"r"` — runnable R script with commented output |
| 108 | + - `"html"` — HTML |
| 109 | + - `"rtf"` — rich text for presentations |
| 110 | + - `session_info = TRUE` — append `sessionInfo()` / `sessioninfo::session_info()`, |
| 111 | + so versions travel with the reprex (set this when the bug may be |
| 112 | + version-dependent). |
| 113 | + - `std_out_err = TRUE` — capture stdout/stderr too (e.g. `system()` / |
| 114 | + subprocess or C-level output that doesn't come back as normal R results). |
| 115 | + - `wd =` — set the working directory when the code needs one. |
| 116 | + - Validation bonus: because `reprex()` runs in a fresh session, if it errors |
| 117 | + on a missing object or package, your example wasn't actually |
| 118 | + self-contained — fix that before sharing. |
| 119 | + |
| 120 | + Companion helpers handle "wild-caught" reprexes (all exported in reprex |
| 121 | + 2.x): `reprex_clean()` (strip the `#>` output markers from a rendered/pasted |
| 122 | + reprex, leaving runnable code), `reprex_rescue()` (recover code from |
| 123 | + R-console output with `>`/`+` prompts), and `reprex_invert()` (the inverse |
| 124 | + of `reprex()` — recover the input code from a rendered reprex). |
| 125 | +- When the bug might be **version-dependent**, capture `sessionInfo()` (or set |
| 126 | + `session_info = TRUE` above) in the reprex so versions are part of the |
| 127 | + record. If you suspect *stale* packages are the cause, |
| 128 | + `tidyverse::tidyverse_update()` outside the reprex can rule that out — but it |
| 129 | + updates packages, it doesn't |
| 130 | + record versions, so don't put it in the reprex itself. |
| 131 | +- Build artifacts (`_site/`, `_freeze/`, `.quarto/`) are common confounders |
| 132 | + for "it renders differently" bugs — a clean standalone render sidesteps |
| 133 | + stale freeze caches. |
| 134 | + |
| 135 | +## Before declaring the reprex good |
| 136 | + |
| 137 | +- A fresh session runs it top-to-bottom and shows the phenomenon (and nothing |
| 138 | + else breaks first). |
| 139 | +- It references no object, file, or option it didn't create itself. |
| 140 | +- It's as small as you can make it and still reproduce. |
| 141 | + |
| 142 | +## Don't |
| 143 | + |
| 144 | +- Don't paste the entire app/module — that's the opposite of a reprex. |
| 145 | +- Don't commit scratch reprex files; keep them in `/tmp` (or `tempdir()` on |
| 146 | + non-Unix machines) or a gitignored scratch path. |
| 147 | +- Don't iterate fixes in the slow full-context loop once you have a reprex |
| 148 | + that reproduces. |
0 commit comments