<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Island94.org</title>
    <description>A Lost and Found</description>
    <link>https://island94.org/</link>
    <atom:link href="https://island94.org/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Tue, 11 Aug 2026 15:27:00 +0000</pubDate>
    <generator>Ruby on Rails</generator>
      <item>
        <title>Two interviews and Information Technology thoughts</title>
        <description>
          <![CDATA[
          <p><em>The following is one of those posts where I share some concrete memories/experiences that I associate with some musings, but otherwise doesn’t really have a strong point other than I want to write it down.</em></p>

<p>When I worked at Code for America, I helped design and deliver two different technical interviews.</p>

<p><strong>The first interview</strong>, which I feel confident I can take credit for wholly, was security related. Me, the interviewer, would first have the candidate read about <a href="https://portswigger.net/kb/issues/00500400_cross-domain-referer-leakage">Cross Domain Referer Leakage</a>, and then we’d talk about it:</p>

<ul>
  <li>How would you summarize the vulnerability in your own words?</li>
  <li>Describe to me a scenario for how an attacker would exploit this.</li>
  <li>Can you think of any apps or services you’ve used or worked on that could be vulnerable to this?</li>
  <li>What are some various ways that we might mitigate this? Are there any non-code changes, or things that you would estimate as more or less effort? Thinking of a typical app you’ve worked on, how might we address it?</li>
</ul>

<p>…and then we’d go into an actual Ruby on Rails codebase, pinned to a specific commit from whence the codebase had that specific class of vulnerability, and talk through making a code change together to mitigate it, if not actually making the change then and there with tests and so forth (time permitting).</p>

<p>I particularly liked this interview because it involved some technical reading comprehension, the implication that project responsibility was broad (hey, you’re gonna have to be aware of things!), and the particular problem itself I find very technically satisfying because it involves weird browser security model stuff (but still simple, relatively speaking) and also the solution involves booping around redirects, which I think is fun.</p>

<p>As background, Code for America at that time was largely a Ruby on Rails shop, though we tried to be programming language and framework agnostic during the hiring process (to my mild disagreement). That said, we did emphasize experience with full-stack frameworks similar to Rails, like Laravel or Django or Java Spring, and especially emphasized full-stack <em>web development</em>. We were also, at the time, heavily pair-programming oriented (lots of former Pivotal Labs peeps), so these interviews were performed in front of dual-monitors+keyboards (or remotely with remote control) and the emphasis was on clearly communicating intent as much as it was on manipulating the code.</p>

<p><strong>The second interview</strong>, which I don’t remember if it was my original design, or if I simply spent a lot of time shaping it over the years, was more of a traditional “here’s a feature ticket, let’s do it together.” That feature was like “As a user of the dashboard, I want to be able to filter [this list of records] by those authored by a specific user within our organization.” I think the provided design was maybe a picture of the page and a “filter widget goes here”-arrow below the heading and above the list of records.</p>

<p>During this interview, the main beats were like:</p>
<ul>
  <li>For the visual affordance, what makes sense? And hopefully they’d ask about the design system and we’d talk about how many potential users there might be and I’d guide them towards like “a select dropdown”.</li>
  <li>How is it manipulated by a user and how does the select option value get transported to the backend server? (does it auto submit, is there a submit button)</li>
  <li>How do we filter it on the backend and display the result? Are there any validation or security or record lifecycle considerations we should make?</li>
</ul>

<p>The last question always had a lot of substance to it, because it would be like “well, if we use their username as the value, what happens when they change their username?”, or “What happens if someone slaps in a user that isn’t a member of the org?” and so forth. All the sorts of things you’d hopefully enjoy nerding out about with a (potential) longterm colleague.</p>

<p>I’d always finish the second interview with an open ended question of like: <em>Now that you’ve seen some of our administrative interface and we’ve talked about the data and uses… and thinking of your own experience building and using applications generally… what ideas come to mind for how we might further improve the functionality and usability of this interface?</em></p>

<p><strong>And this is the bit where I think about information technology.</strong> Because a lot of the thing I’m trying to evaluate for people (engineers, but also designers and product managers and pretty much anybody working adjacent to “tech”) is <strong><em>What would you say we are even doing here? Like in the big sense?</em></strong></p>

<p>And to answer that myself, it’s sorta like the <a href="https://island94.org/2025/08/everything-i-know-about-ai-learned-by-reading-the-aws-bedrock-client-ruby-sdk-code">Pushing Patrick I’ve mentioned before</a>: we’re taking a data-based representation of something from over here… and then we’re putting it over there. And along the way the thing we’re building is:</p>

<ul>
  <li>First off, we’re being <strong>reductive</strong> by taking a conceptual thing and reducing it to some limited representation of it in the data</li>
  <li>Then it’s being <strong>collected</strong> and <strong>aggregated</strong> so that some subset (or <em>all</em> of it, universally) is in our system</li>
  <li>It’s being <strong>enriched</strong> in some way by adding additional internal or external data to it. And maybe being <strong>conflated</strong> or <strong>deduplicated</strong> as well. And maybe this is happening <strong>explicitly</strong> by additional data entry, or maybe <strong>implicitly</strong> by some behavior of the system. Maybe <strong>immediately</strong> or <strong>over some time horizon, short, long or in perpetuity</strong>
</li>
  <li>And then it’s being <strong>listed</strong> and <strong>filtered or scoped</strong> and then specific parts of the data are being <strong>projected</strong> or <strong>displayed</strong> in lots of different ways.</li>
  <li>And all together it’s ultimately driving towards some sort of <strong>monitoring and decisional process</strong> that leads to an <strong>action</strong> either within or outside the system.</li>
</ul>

<p>And then for those of us building it, we’re trying to figure out how to do it in a way that is <strong>functional (it works)</strong>, and <strong>usable</strong>, and <strong>ultimately adds value</strong> by being <strong>faster</strong> or <strong>cheaper</strong> or <strong>better</strong> or simply <strong>more possible</strong> than it would be if people did this without the Information Technology thing being built (e.g. <strong>than the status quo</strong>).</p>

<p>And you might be thinking: <em>hey Ben, that sounds like more of a business, product, and design problem than a software engineering problem.</em> And yes, I would agree with you somewhat, but I sorta think the “Software Engineering” domain is pretty uninteresting when untethered from the rest of it. As well as the thought: any backend, non-user facing system or library or object or function still has users: those users are the developers who have adopted or integrated or wrote and maintain the code that calls it. Same same.</p>

<p>I can’t help but think, nearly every time I write a <code>predicate?</code> method of <a href="https://thoughtbot.com/blog/umbrella-today">Umbrella Today</a> (<a href="https://island94.org/2011/06/data-divides-and-umbrellafication">good and bad</a>).</p>

<p><img src="https://island94.org/uploads/2026/umbrella-today.jpg" alt="Screenshot of the Umbrella Today website answering “Umbrella Today?” with a large “YES”"></p>

<p>I’ll offer that there is a dimension I have been thinking a lot about how it integrates into that previous definition of Information Technology, and that’s <strong>stickiness and engagement</strong>. On one hand, if you’re getting value out of one side of it, then it shouldn’t be soul-crushing to use. But if you’re using it purely for the <strong>delight and high</strong> and the actual value or benefit is fleeting… that seems… bad?</p>

<p>One reason these things come to mind is that I’m coming to the <a href="https://github.com/codeforamerica/civic-tech-patterns">15 year anniversary of writing Civic Tech Patterns</a>, which has been a longterm project to bully a very small group of very bright people into not wasting their time (I choose these words very specifically; it’s not an approach I would choose from scratch today). At the heart is this same project of trying to engage people in “no really, what are you trying to do here?”</p>

<p>And with AI agents and generative chatbot agents and the like too, where many of the experiences of “Information Technology” (as I define it above) is put into a black box and what comes out is often… bad? But also sticky and delightful? For some people? Arguably? I dunno.</p>

<p>If anything, in addition to these thoughts pervading my own direct work, I think of this moment where:</p>

<ul>
  <li>More and more people are ostensibly building “Information Technology”-style tools (with generative tools) and I see a potential gulf between <em>looks like</em> a thing that has historically performed Information Technology and (what I believe) is the actual goal of <em>performs the function of, really well,</em> Information Technology.</li>
  <li>Maybe my conceptualization of Information Technology is lacking something, or doesn’t quite encompass this new Generative+Agent thing many folks are trying to make happen. Of which I think Information Technology is ultimately about <strong>converge</strong> and Generative+Agent stuff seems more like <strong>diverge</strong> but I dunno.</li>
</ul>

<p>Hence my writing this out, cause on that last bit, I dunno. And given how rarely I seemed to interview people who <em>got it</em> (at least according to me) for the last generation of Information Technology, I can’t say that it’s much of a change if this next generation doesn’t get it either. And probably that’s ok.</p>

<p><strong>Random note:</strong> While looking through my (other) notebook, I found shorter commentary along similar lines that I <a href="https://www.reddit.com/r/webdev/comments/qlheml/comment/hj5s9ae/">commented on Reddit in reply to someone being grumpy about producing a portfolio of side projects</a>:</p>

<blockquote>
  <p>I’m not the commenter you’re responding to. But I am also a hiring manager and want to try to meet both of you in the middle.</p>

  <p>The thing that I want to evaluate, by seeing some non-school projects, is that the candidate has an understanding of how <em>Information Technology</em> can be used to solve a problem. Some of that is technological (does it work), and some of it is telling the story (eg input or take data and transform it in a new or novel way that solves a human-understandable problem). It’s “design” to the end of demonstrating you understand the overall problem space of how technology is applied to a problem. And calibrated to what’s expected of someone coming out of a (effective) bootcamp.</p>

  <p>And when I’ve got 150 candidates for an entry level position, the candidates who can communicate that effectively will stand out.</p>
</blockquote>

          ]]>
        </description>
        <link>https://island94.org/2026/08/two-interviews-and-information-technology-thoughts</link>
        <guid isPermaLink="true">https://island94.org/2026/08/two-interviews-and-information-technology-thoughts</guid>
        <pubDate>Tue, 11 Aug 2026 15:27:00 +0000</pubDate>
      </item>
      <item>
        <title>How to programmatically upload attachments to GitHub Issues, Pull Requests, and Comments, finally, for now</title>
        <description>
          <![CDATA[
          <p>It’s possible to programmatically upload images to GitHub Issues, Pull Requests and Comments via automation. Finally, though maybe mistakenly, I dunno.</p>

<p>It’s fairly well known that for the longest time, <a href="https://github.com/cli/cli/issues/1895">GitHub has not had a programmatic interface for uploading images and attachments</a>. It’s possible in the browser, by dragging-and-dropping your image, but not via something you can script or curl. That’s been a bummer for lots of GitHub Action-powered systems you might imagine, like: automatically attaching demo screenshots or videos on PRs for new features, or attaching failure screenshots or visual diffs from browser tests.</p>

<p>Until now, I guess. Here’s the unofficial, undocumented endpoint:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">FILE</span><span class="o">=</span>cat.jpg
<span class="nv">MIME</span><span class="o">=</span>image/jpeg
<span class="nv">REPOSITORY</span><span class="o">=</span>bensheldon/good_job
<span class="nv">TOKEN</span><span class="o">=</span><span class="si">$(</span>gh auth token<span class="si">)</span>

curl <span class="nt">-s</span> <span class="s2">"https://uploads.github.com/user-attachments/assets?name=</span><span class="nv">$FILE</span><span class="s2">&amp;content_type=</span><span class="nv">$MIME</span><span class="s2">&amp;repository_id=</span><span class="si">$(</span>gh api <span class="s2">"repos/</span><span class="nv">$REPOSITORY</span><span class="s2">"</span> <span class="nt">--jq</span> .id<span class="si">)</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-X</span> POST <span class="nt">-H</span> <span class="s2">"Authorization: Bearer </span><span class="nv">$TOKEN</span><span class="s2">"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Accept: application/json"</span> <span class="nt">--data-binary</span> <span class="s2">"@</span><span class="nv">$FILE</span><span class="s2">"</span>

<span class="c"># that outputs a JSON response containing the attachment URL, which you can then embed in an image tag into the GitHub content.</span>
</code></pre></div></div>

<p>That’s it! I mean, you can be more clever than curl or whatever, but that’s the idea. It’s equivalent to dragging-and-dropping an image into an Issue, PR, or Comment.</p>

<p>I don’t know if this endpoint always accepted an Auth Token and no one tried or noticed or talked about it, or if that’s new. My memory was that this endpoint <em>only</em> previously accepted Cookie Auth: circa 2018 I explored different Visual Diff services and the lack of a programmatic API for GitHub attachments meant that to DIY you’d have to use a service like S3, and then a service to authorize S3, and that’s annoying to set up when GitHub Actions could do everything else. I’m pretty sure I tried to fuzz the attachment interface at the time and it didn’t work.</p>

<p>I recently learned all of this was possible because <a href="https://shakacode.com/">Justin Gordon of Shakacode</a> shared a link to <a href="https://github.com/intercom/2x-skills/blob/59213af0a2db9321ef10355ff24e9bd619151b6b/plugins/pr-tools/skills/attach-github-assets/SKILL.md">Intercom’s 2x-skills <code>attach-github-assets</code></a> and I was… skeptical… and then I tried it and it worked!</p>

<p><img src="https://island94.org/uploads/2026/github-attachment-upload-demo.png" alt="Ben Sheldon asking Claude to use the attach-github-assets skill to upload a placekitten image as a comment to a GitHub issue, and Claude successfully doing so"></p>

<p>It’s very useful, I hope it doesn’t go away.</p>

          ]]>
        </description>
        <link>https://island94.org/2026/08/programmatically-upload-attachments-to-github-issues-pull-requests-comments</link>
        <guid isPermaLink="true">https://island94.org/2026/08/programmatically-upload-attachments-to-github-issues-pull-requests-comments</guid>
        <pubDate>Mon, 03 Aug 2026 17:00:00 +0000</pubDate>
      </item>
      <item>
        <title>Running code when the Ruby on Rails webserver boots</title>
        <description>
          <![CDATA[
          <p>I was recently part of a <a href="https://github.com/mastodon/mastodon/pull/39990">Mastodon discussion</a> about the best way to run code in a Rails application before Puma boots. The need for this is that sometimes you want some code that <em>only</em> runs within a webserver, not when running rake tasks or rails commands or opening the console.</p>

<p>Here we go…</p>

<h3 id="via-server-do-blocks">Via <code>server do</code> blocks</h3>

<p>Rails 6.1 added a <a href="https://github.com/rails/rails/pull/39953"><code>Railtie#server</code> hook</a> that can be invoked in <code>application.rb</code> or a gem’s <code>engine.rb</code>. It’s invoked when the <code>config.ru</code> file is evaluated.</p>

<p><strong>Warning:</strong> the invocation of the hook in <code>config.ru</code> is missing from <em>a lot</em> of Rails projects. I once <a href="https://github.com/rails/rails/pull/49704">lackadaisically tried to address this upstream</a>, but doublecheck your <code>config.ru</code> actually contains <code>Rails.application.load_server</code> as <a href="https://github.com/rails/rails/blob/b83739f81cc9f0f8028d98e1a3d7f6f79c375fb5/railties/lib/rails/generators/rails/app/templates/config.ru.tt#L6">it should</a>.</p>

<h3 id="in-your-configru">In your <code>config.ru</code>
</h3>

<p>The Rackup configuration file only gets evaluated by a webserver, so that is a natural place to invoke your webserver-only code.</p>

<p><strong>Aside:</strong> like <code>boot.rb</code>, I either find <em>interesting things</em> lurking around in <code>configu.ru</code> , or it’s one of those files that hasn’t been touched since <code>rails new</code> was first run.</p>

<h3 id="as-a-puma-plugin">As a Puma plugin</h3>

<p>If you’re dedicated to Puma, <a href="https://github.com/puma/puma/blob/b8341dc946f0a2444e5ea6730d1967ca53c8d006/docs/plugins.md">write a simple plugin for it</a>. Puma plugins also allow hooking into more sophisticated places than simply “before it starts”.</p>

<p>tbh, I wasn’t aware of Puma plugins as an integration point until <a href="https://github.com/rails/solid_queue/blob/86f3d92f1dd68547ec0ebe960fc9933c203d9e51/lib/puma/plugin/solid_queue.rb">Solid Queue shipped a Puma plugin</a> for its in-webserver mode.  I like it!</p>

<h3 id="by-searching-the-callstack">By searching the callstack</h3>

<p>I just mentioned Solid Queue has having an admirable integration point. My GoodJob has an… ok one: <a href="https://github.com/bensheldon/good_job/blob/81db22b7cfde2579b2d735e076fa6eaacc65affa/lib/good_job/configuration.rb#L408-L418">searching the callstack for webserver fingerprints</a>. I chose this path at the time for the broadest zero-configuration compatibility with older versions of Rails, different webservers, and JRuby, but I wouldn’t recommend it very strongly today because it feels brittle:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">in_webserver?</span>
  <span class="k">return</span> <span class="vi">@_in_webserver</span> <span class="k">unless</span> <span class="vi">@_in_webserver</span><span class="p">.</span><span class="nf">nil?</span>

  <span class="vi">@_in_webserver</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">const_defined?</span><span class="p">(</span><span class="ss">:Server</span><span class="p">)</span> <span class="o">||</span> <span class="k">begin</span>
    <span class="n">self_caller</span> <span class="o">=</span> <span class="nb">caller</span>
    <span class="n">self_caller</span><span class="p">.</span><span class="nf">grep</span><span class="p">(</span><span class="sr">%r{config.ru}</span><span class="p">).</span><span class="nf">any?</span> <span class="o">||</span> <span class="c1"># EXAMPLE: config.ru:3:in `block in &lt;main&gt;' OR config.ru:3:in `new_from_string'</span>
      <span class="n">self_caller</span><span class="p">.</span><span class="nf">grep</span><span class="p">(</span><span class="sr">%r{puma/request}</span><span class="p">).</span><span class="nf">any?</span> <span class="o">||</span> <span class="c1"># EXAMPLE: puma-5.6.4/lib/puma/request.rb:76:in `handle_request'</span>
      <span class="n">self_caller</span><span class="p">.</span><span class="nf">grep</span><span class="p">(</span><span class="sx">%{/rack/handler/}</span><span class="p">).</span><span class="nf">any?</span> <span class="o">||</span> <span class="c1"># EXAMPLE: iodine-0.7.44/lib/rack/handler/iodine.rb:13:in `start'</span>
      <span class="p">(</span><span class="no">Concurrent</span><span class="p">.</span><span class="nf">on_jruby?</span> <span class="o">&amp;&amp;</span> <span class="n">self_caller</span><span class="p">.</span><span class="nf">grep</span><span class="p">(</span><span class="sr">%r{jruby/rack/rails_booter}</span><span class="p">).</span><span class="nf">any?</span><span class="p">)</span> <span class="c1"># EXAMPLE: uri:classloader:/jruby/rack/rails_booter.rb:83:in `load_environment'</span>
  <span class="k">end</span> <span class="o">||</span> <span class="kp">false</span>
<span class="k">end</span>
</code></pre></div></div>

<h3 id="in-a-rack-middleware">In a Rack middleware</h3>

<p>This is the simplest answer: if you only want code to run in the webserver, invoking it on the first (or any) request makes sense to me. In Ruby on Rails, I think the development environment’s “are there pending migrations?” check is a neat example because Rails does the check in the request, and then raises a <a href="https://github.com/rails/rails/blob/b83739f81cc9f0f8028d98e1a3d7f6f79c375fb5/activerecord/lib/active_record/migration.rb#L147-L156">special kind of <code>ActionableError</code></a> that causes the “Run pending migrations” button to appear.</p>

<p><strong>Aside:</strong> I think people are sometimes too quick to reach for Rack middleware instead of doing a Controller <code>before_action</code>; consider that too.</p>

<h3 id="other-thoughts">Other thoughts</h3>

<p>There is a lot of trickiness to this problem. Mastodon is sorta special because it’s an <em>application that is distributed</em>. As opposed to your own specially customized Rails application you’re wholly developing, or a narrow gem/engine that you’re distributing to be installed in someone else’s application.</p>

<p>It’s tough to recommend a single best option. For a gem or engine, like Solid Queue or GoodJob, you’re probably focused most on the simplest and trouble-free installation with the widest compatibility. For your own application, the goal is probably not leaving a forgotten footgun to be stumbled over in the future. Good luck!</p>

          ]]>
        </description>
        <link>https://island94.org/2026/07/running-code-when-the-ruby-on-rails-webserver-boots</link>
        <guid isPermaLink="true">https://island94.org/2026/07/running-code-when-the-ruby-on-rails-webserver-boots</guid>
        <pubDate>Fri, 31 Jul 2026 18:43:00 +0000</pubDate>
      </item>
      <item>
        <title>Planning badly, more and less</title>
        <description>
          <![CDATA[
          <p>The following is some notes I’ve collected over my twenty years of career, some in tech, some in nonprofit and academia and government, and some as a strategic planning consultant.</p>

<p>I’ll cover the most common stumbles I’ve seen, and end with a little bit of advice for doing better. This is not intended to be an essay, or some something comprehensive like <a href="https://www.goodreads.com/book/show/11721966-good-strategy-bad-strategy"><em>Good Strategy, Bad Strategy</em></a>. Just fun stuff I occassionally try to describe to others.</p>

<p>I do recognize that a lot of these things stem from context like “toxic workplaces are bad and warp your sense of normalcy long after you leave them”, so if any of these things I write about seems self-evidently obvious, consider yourself lucky.</p>

<h3 id="rule-of-three">Rule of three</h3>

<p>A simple heuristic for identifying bad planning, either from yourself or others, is how closely it hews to the Rule of Three. For example, if you’re fitting your planning to OKRs, and you end up with three Objectives, each with three Key Results, maybe someone in that process is a little bit lazy.</p>

<p><a href="https://en.wikipedia.org/wiki/Rule_of_three_%28writing%29">People like threes</a>. If you turn in a one item plan, maybe it looks like you didn’t do the assignment; if you submit 5 or more, maybe you don’t look focused enough. I would suggest you play with it to see if that’s actually the case rather than <em>always</em> trying to shape <em>everything</em> down to 3: try defensibly submitting just one priority, or twelve, and see what happens.</p>

<p>When I worked at Code for America, there was a period where they adopted a planning framework called OGSM: Objectives, Goals, Strategies and Measures. Unfortunately, its application led to teams creating massive spreadsheets of 3 goals by 3 objectives by 3 Strategies by 3 Measures each. 3-to-the-4th-power is 81. Yikes. This was eventually reined in.</p>

<p>So to the extent that planning habitually hovers around 3 items per category, that’s the simplest sign that “planning” is a box-checking and template-filling exercise. Either by you, or by the people reviewing it.</p>

<h3 id="operational-planning-and-strategic-planning">Operational Planning and Strategic Planning</h3>

<p>What’s the point of planning?</p>

<ul>
  <li>To capture the entirety of people’s work and responsibilities and ensure there is proper direction and resources to achieve it all …or…</li>
  <li>To apply an unnatural focus upon a critical opportunity or risk that might not otherwise get the attention it needs to be addressed.</li>
</ul>

<p>I call the former Operational Planning, and the latter Strategic Planning. I do Strategic Planning consultation, and a good 20% of my time is spent ensuring that the thing we’re doing together <em>is</em> Strategic rather than Operational.</p>

<p>Both Operational Planning and Strategic Planning are important and necessary. But you gotta keep them distinct. Especially as planners not-infrequently pad out ostensibly “Strategic” plans with activities that would happen anyways due to momentum and natural care (gotta get to three!).</p>

<p>Most of this post is about my experience trying to do Strategic Planning well.</p>

<h3 id="who-appears-in-a-plan-not-necessarily-everyone">Who appears in a plan? Not necessarily everyone.</h3>

<p>A common problem I see with Strategic Planning is a necessity that <em>everyone’s</em> work show up in the plan. I experienced this a lot working in matrixed, cross-functional teams, when every function (Design, Engineering, Support) wanted—if not needed, to justify themselves to others—their work to show up in the strategy for the cross-functional team as a whole.</p>

<p>In good Strategic Planning, not everyone’s primary focus will show up in the overall strategy all the time. Sometimes that looks like this: we’re going to continue operating effectively on our own goals in order to create space for other parts of the team to have a strategic focus. That <em>should</em> be ok!</p>

<h3 id="roll-up-or-roll-down-dont-wait">Roll up or roll down? Don’t wait.</h3>

<p>At nearly every tech job I’ve worked, there was the intent that company strategy would be set at the top, by leadership, and then that strategy would roll down to sub-organizations and departments who would do a planning process to align with the company strategy.</p>

<p>That “roll-down” never happened effectively. Instead, the status quo was that individual teams would kinda take a temperature check, read a few recent company memos, and write their own plans based on their own internal goals, and wash those team goals through the contemporary language of the company. Then the department would take those and kind of generalize them into three-ish themes, and that would continue rolling up and never be heard from again.</p>

<p>The most disfunctional times was when teams would wait to plan in the expectation of some company-wide process that never came. And then would be caught flat-footed and disorganized when they were inevitably asked “What is your focus right now? What would extra resources help you achieve? How might you absorb a resource cut?”</p>

<p>The point being here, I would never recommend waiting for inputs to planning. Just do it, and then align with whatever the official process is.</p>

<h3 id="planning-should-be-sufficient">Planning should be sufficient</h3>

<p>Some of the most annoying things in my career is seeing my own leadership, or an adjacent team, be casually asked by someone even higher up some flavor of: “what are you doing to work towards [new company priority]?” and the sub-leader immediately kicks off some new planning process to preempt the old one.</p>

<p>Have some backbone, and simply remind them of current plan. I think a lot of the time no one honestly remembers what was previously planned and they’re just making chit-chat. If you have a quarterly planning process, then any new company priority gets fed as input into the next planning cycle (unless it’s actaully pants-on-fire/company-runs-out-of-money-otherwise situation, which is rare),</p>

<h3 id="measures-and-targets-are-different">Measures and Targets are different</h3>

<p>I’ve observed people try to find a nihilism between the statements “You can’t manage what you don’t measure” and “When a measure becomes a target, it ceases to be a good measure” (Goodhart’s Law). Whereas I think you just have to allow both to be true at once and allow “target” and “measure” to mean different things.</p>

<p>The Target is what you aim directly at. A Measure is just something you look at occassionally or from the side of your eye and say “huh, interesting”.</p>

<p>This distinction isn’t particularly important, other than to be able to distinguish one from the other, and also recognize when someone with power over you is “huh, interesting” a little too hard and you should pay attention.</p>

<h3 id="progress-and-percentage-targets">Progress and percentage targets</h3>

<p>For anything that you’re asked to track as “percentages to completion” you need to ask whether the intent is that you reach 100% or there’s some neg-style “if you hit 100% that means you weren’t audaciaous enough in setting your target” and thus you need to take your actual target and inflate/deflate it the expectation.</p>

<p>The outcome you want is both that you know what you’re trying to achieve (regardless of what you publicly commit to) and you look good doing it.</p>

<p>My advice for anyone in charge is to simply make 100% the achievable target and avoid all of this triangulative nonsense.</p>

<p>Percentages are also annoying when you’re measuring progress towards some boolean done-or-not-done where there’s no benefit until it’s done. 80% of zero value is still zero value.</p>

<p>In my opinion, the entire reason to report on progress is to have some shared context for if or when you need to ask for more resources to finish it out. Same with stoplights. If the only shared context is “things were green but now they’re yellow” that’s not useful; in the same way as “if some projects aren’t yellow, that means we’re not working hard enough” sucks too. Also when percentages or stoplights get averaged and rolled-up 🙄.</p>

<h3 id="writing-a-plan-vs-doing-planning">Writing a plan vs doing planning</h3>

<p>The most common pattern I see for“planning enablement” is the company provides a template for the final document. e.g. fill out this template and turn it in and that’s your plan.</p>

<p>Template-first planning means that a planning process then becomes:</p>

<ol>
  <li>First, filling in the “what are you going to do?” section</li>
  <li>Second, making up something narratively consistent to fill out everything else</li>
</ol>

<p>I see this happen everywhere. People already have stewing what they want to do, and then everything else is backwards justification.</p>

<p><em>This backwards-justification is one of the key dynamics that we’re going to try to overcome as I now shift towards <strong>how I think you should plan instead</strong>.</em></p>

<h3 id="how-i-think-you-should-plan-instead">How I think you should plan, instead</h3>

<p>The main idea in how I think you should plan is that you break the planning down into stages where you try to get farther and farther away from “what people want to do” and towards a more complex “what needs to be done to overcome the barriers that make it hard for people to do the things they want to do”. Got all that? Good.</p>

<p><em>Aside: None of this is my new idea. Creating conceptual distance is core part of <a href="https://en.wikipedia.org/wiki/Synectics">Synectics</a> (yes, Steve Krug once mentions it) and a lot of this process comes from taking lots of trainings in <a href="https://www.top-training.net/w/">Technology of Participation</a>, which has <a href="https://www.amazon.com/Getting-Bottom-ToP-Methodologies-Participation/dp/1532033680">entire books on gestalt theory</a> and such.</em></p>

<p>So of separate stages, that means that you force yourself to do each piece separately, rather than starting at the end (“what you want to do”) and working backwards or all at once.  The stages are:</p>

<ol>
  <li>
<strong>What’s going on?</strong> (the dry name here is “Environmental Scan”)  Laundry list any new themes, priorities, developments that have happened inside the company and outside it since the last planning process. Go look at the last plan too. “We said we were gonna do X” is a good thing to list here. <em>The thing to be aware of is that people are dying to shout “what we are going to do” so everything at this stage is still somewhat suspect. But that’s fine, we recognize thatand move forward.</em>
</li>
  <li>
<strong>What are we going to do?</strong> That’s right, get it out there. Shake it out. List the things you want to do. Yes, you’re going to do them. They are very important. They are! I think it’s really important at this stage to let people linger and make sure they see those things they want to do captured. These are the things that are going to happen.</li>
  <li>
<strong>What makes it hard to do those things? (“Barriers”)</strong> Like, really, why are they such big things that you felt the need to articulate them so badly. There’s lots of important work that just happens, so why spotlight those things? What are the problems you’re trying to overcome by spotlighting them? This is usually like resource problems, or distractions, or actual disagreements between leaders, or some other problem that constantly steals focus. Remember, you’re not trying to <em>justify</em> the thing from the previous stage, you’re trying to explain why you would ever need to justify them in the first place. Why can’t the work just happen?</li>
  <li>
<strong>Of those things that make it hard, what do you have control over?</strong> It’s possible to work this question into the previous stage, but I like to allow people to let loose with all their grievances before trying to narrow them down slightly. This is a good spot though to fully divorce people from the “what I want to do”-brainspace by engaging them with something else to problem solve around….</li>
  <li>
<strong>Of the barriers, what can you do to overcome or route around them?</strong> Here we go, this is what we’ve been working towards: solving problems. What actually would address the problems we identified in the previous stage? Write that down.</li>
  <li>
<strong>Of those solutions, what are you going to do? (“The Strategics”)</strong> This is the decisional commitment. Remember, we haven’t replaced the initial stage’s “what are we going to do’s”—those are still happening—but rather worked towards a new list of “everything would sure be a lot better/faster/stronger if we did this first or in-addition-to”.</li>
</ol>

<p>…and then you write that into the planning template to report it out. And in this case, the “justification” for doing The Strategics is listing all of the stuff that you were going to do anyway.</p>

<h3 id="but-you-can-do-this-badly-too-right">But you can do this badly too, right?</h3>

<p>Sure of course, we can do <em>anything</em> badly. It’s possible to wash the initial “what are are we going to do’s” through this entire process and end up exactly where you started, but with several hundred words of retcon around it. That happens.</p>

<p>…but at least I believe this process affords some potential of not doing that, whereas I think most default planning processes (fill out the template) almost guarantee you end up where you started.</p>

          ]]>
        </description>
        <link>https://island94.org/2026/07/planning-badly-more-and-less</link>
        <guid isPermaLink="true">https://island94.org/2026/07/planning-badly-more-and-less</guid>
        <pubDate>Tue, 14 Jul 2026 15:43:00 +0000</pubDate>
      </item>
      <item>
        <title>One year of Ruby on Rails configuration</title>
        <description>
          <![CDATA[
          <p>I’ve been working professionaly with Ruby on Rails for nearly 15 years (I’m also the author of GoodJob and Spectator Sport). Last year I left GitHub and co-founded a technology startup, <a href="https://www.openfrontdoor.com/">Frontdoor Benefits</a>, that helps people enroll and manage their US government welfare benefits like SNAP/EBT.</p>

<p>Therefore, I’ve been working in a fresh Ruby on Rails app full-time now for 1 year. One of the Rails pillars is <a href="https://rubyonrails.org/doctrine">“convention over configuration”</a>, so I thought it would be fun to share what has so far accumulated in my app’s <code>/config</code> directory: monkeypatches, extensions, and appwide behaviors.</p>

<p>Let’s start with the most controversial one.</p>

<h3 id="objectnot_nil-and-boolean-extensions">
<code>Object#not_nil?</code> and boolean extensions</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/extensions/ext_object_boolean_nil.rb</span>

<span class="k">class</span> <span class="nc">Object</span>
  <span class="k">def</span> <span class="nf">not_nil?</span>
    <span class="o">!</span><span class="nb">nil?</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">false?</span>
    <span class="kp">false</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">true?</span>
    <span class="kp">false</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">FalseClass</span>
  <span class="k">def</span> <span class="nf">false?</span>
    <span class="kp">true</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">TrueClass</span>
  <span class="k">def</span> <span class="nf">true?</span>
    <span class="kp">true</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I realize there are PhDs written about the evils of null. I DISAGREE. I am continually confronted with the need to distnguish between “Yes”, “No”, and “has not answered the question yet” in my my <a href="https://island94.org/2025/04/wide-models-and-active-record-custom-validation-contexts">wide models</a>. I want a simple predicate pair like <code>present?</code>/<code>blank?</code> for <code>nil</code> , hence <code>nil?</code> /<code>not_nil?</code> .</p>

<p>And then you can see where that led me wanting to distinguish between truthy and falsey and <em>true</em> true and <em>false</em> false. Predicates are great!</p>

<h3 id="timestamptz-default">
<code>timestamptz</code> default</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/active_record_timezones.rb</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:active_record_postgresqladapter</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">ConnectionAdapters</span><span class="o">::</span><span class="no">PostgreSQLAdapter</span><span class="p">.</span><span class="nf">datetime_type</span> <span class="o">=</span> <span class="ss">:timestamptz</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I write my database migrations using <code>table.datetime</code> and it generates <code>timestamptz</code> columns.  I guess this is fine.</p>

<h3 id="capybarathreadsafe"><code>Capybara.threadsafe</code></h3>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/capybara.rb</span>

<span class="no">Capybara</span><span class="p">.</span><span class="nf">threadsafe</span> <span class="o">=</span> <span class="kp">true</span>
</code></pre></div></div>

<p>I use Capybara as my harness for webdriving automations, rather than writing raw Selenium (never again!).</p>

<h3 id="custom-types">Custom Types</h3>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/custom_types.rb</span>

<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">config</span><span class="p">.</span><span class="nf">to_prepare</span> <span class="k">do</span>
  <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Type</span><span class="p">.</span><span class="nf">register</span><span class="p">(</span><span class="ss">:phone_number</span><span class="p">,</span> <span class="no">PhoneNumber</span><span class="o">::</span><span class="no">Attribute</span><span class="p">)</span>
  <span class="no">ActiveModel</span><span class="o">::</span><span class="no">Type</span><span class="p">.</span><span class="nf">register</span><span class="p">(</span><span class="ss">:phone_number</span><span class="p">,</span> <span class="no">PhoneNumber</span><span class="o">::</span><span class="no">Attribute</span><span class="p">)</span>
<span class="k">end</span>

<span class="k">module</span> <span class="nn">Kernel</span>
  <span class="k">def</span> <span class="nf">PhoneNumber</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="c1"># rubocop:disable Naming/MethodName</span>
    <span class="n">value</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">PhoneNumber</span><span class="p">)</span> <span class="p">?</span> <span class="n">value</span> <span class="p">:</span> <span class="no">PhoneNumber</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There are several annoyances here:</p>
<ul>
  <li>I have to register custom attributes twice: once for Active Record and once for Active Model.</li>
  <li>I want my types to live in <code>/app</code>, and be autoloaded, but there isn’t a lifecycle hook for that</li>
</ul>

<p>And one celebration: I love Ruby and being able to coerce with<code>PhoneNumber(something)</code></p>

<h3 id="dom_target"><code>dom_target</code></h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/extend_action_view_record_identifier.rb</span>

<span class="k">module</span> <span class="nn">ActionView</span>
  <span class="k">module</span> <span class="nn">RecordIdentifier</span>
    <span class="k">def</span> <span class="nf">dom_target</span><span class="p">(</span><span class="o">*</span><span class="n">objects</span><span class="p">)</span>
      <span class="n">objects</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">object</span><span class="o">|</span>
        <span class="k">if</span> <span class="n">object</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Symbol</span><span class="p">)</span> <span class="o">||</span> <span class="n">object</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">String</span><span class="p">)</span>
          <span class="n">object</span>
        <span class="k">elsif</span> <span class="n">object</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Class</span><span class="p">)</span>
          <span class="n">dom_class</span><span class="p">(</span><span class="n">object</span><span class="p">)</span>
        <span class="k">else</span>
          <span class="n">dom_id</span><span class="p">(</span><span class="n">object</span><span class="p">)</span>
        <span class="k">end</span>
      <span class="k">end</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="no">JOIN</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I write a lot of Turbo and I want to be able to chain together an unlimited list of identifiers like <code>(:admin, client, :outbound, Message, :new)</code> to produce something like <code>admin_client_24_outbound_message_new</code> to pair up Turbo Streams and Broadcasts. I <a href="https://github.com/rails/rails/pull/55204">upstreamed this into Rails 8.1</a> so this is unecessary now🎉</p>

<h3 id="field_error_proc"><code>field_error_proc</code></h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/form_errors.rb</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:action_view</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">ActionView</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">field_error_proc</span> <span class="o">=</span> <span class="nb">proc</span> <span class="k">do</span> <span class="o">|</span><span class="n">html_tag</span><span class="p">,</span> <span class="n">_instance_tag</span><span class="o">|</span>
    <span class="n">html_tag</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Still necessary 🫥</p>

<h3 id="goodjob-and-uuidv7">GoodJob and UUIDv7</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/good_job.rb</span>

<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">good_job</span><span class="p">.</span><span class="nf">execution_mode</span> <span class="o">=</span> <span class="ss">:inline</span> <span class="k">if</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">test?</span>
<span class="k">end</span>

<span class="no">GoodJob</span><span class="p">.</span><span class="nf">preserve_job_records</span> <span class="o">=</span> <span class="kp">true</span>
<span class="no">GoodJob</span><span class="p">.</span><span class="nf">on_thread_error</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">exception</span><span class="p">)</span> <span class="p">{</span> <span class="no">Appsignal</span><span class="p">.</span><span class="nf">send_error</span><span class="p">(</span><span class="n">exception</span><span class="p">)</span> <span class="p">}</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:action_mailer</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">ActionMailer</span><span class="o">::</span><span class="no">MailDeliveryJob</span><span class="p">.</span><span class="nf">retry_on</span> <span class="no">StandardError</span><span class="p">,</span> <span class="ss">wait: :polynomially_longer</span><span class="p">,</span> <span class="ss">attempts: </span><span class="no">Float</span><span class="o">::</span><span class="no">INFINITY</span>
  <span class="no">ActionMailer</span><span class="o">::</span><span class="no">MailDeliveryJob</span><span class="p">.</span><span class="nf">discard_on</span> <span class="no">ActiveJob</span><span class="o">::</span><span class="no">DeserializationError</span>
<span class="k">end</span>

<span class="c1"># **SNIP** Lots of GoodJob cron config</span>

<span class="k">module</span> <span class="nn">ActiveJobUUIDv7</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">)</span>
    <span class="k">super</span>
    <span class="vi">@job_id</span> <span class="o">=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid_v7</span>
  <span class="k">end</span>
  <span class="n">ruby2_keywords</span><span class="p">(</span><span class="ss">:initialize</span><span class="p">)</span>
<span class="k">end</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:active_job</span><span class="p">)</span> <span class="k">do</span>
  <span class="kp">include</span> <span class="no">ActiveJobUUIDv7</span>
<span class="k">end</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:good_job_execution</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">before_create</span> <span class="p">{</span> <span class="nb">self</span><span class="p">.</span><span class="nf">id</span> <span class="o">||=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid_v7</span> <span class="p">}</span>
<span class="k">end</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:good_job_process</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">before_create</span> <span class="p">{</span> <span class="nb">self</span><span class="p">.</span><span class="nf">id</span> <span class="o">||=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid_v7</span> <span class="p">}</span>
<span class="k">end</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:good_job_batch_record</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">before_create</span> <span class="p">{</span> <span class="nb">self</span><span class="p">.</span><span class="nf">id</span> <span class="o">||=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid_v7</span> <span class="p">}</span>
<span class="k">end</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:good_job_setting</span><span class="p">)</span> <span class="k">do</span>
  <span class="n">before_create</span> <span class="p">{</span> <span class="nb">self</span><span class="p">.</span><span class="nf">id</span> <span class="o">||=</span> <span class="no">SecureRandom</span><span class="p">.</span><span class="nf">uuid_v7</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p>A surprising misconception I’ve run across with folks using GoodJob is that using <em>more</em> configuration is better. Less less less.</p>

<p>Specifically at the bottom, I’ve <a href="https://github.com/bensheldon/good_job/issues/1698">patched GoodJob and Active Job to use UUIDv7</a> to see if it’s any better. The verdict is still out. It’s not worse!</p>

<h3 id="i18n-verification">I18n verification</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/i18n_verify</span>

<span class="k">return</span> <span class="k">unless</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">configuration</span><span class="p">.</span><span class="nf">i18n</span><span class="p">.</span><span class="nf">raise_on_missing_translations</span>

<span class="no">IGNORED_I18N_KEYS</span> <span class="o">=</span> <span class="sx">%w[
  activerecord.attributes.client.reports
  ...
]</span><span class="p">.</span><span class="nf">freeze</span>

<span class="k">module</span> <span class="nn">I18n</span>
  <span class="k">class</span> <span class="o">&lt;&lt;</span> <span class="nb">self</span>
    <span class="k">alias</span> <span class="n">original_translate</span> <span class="n">t</span>

    <span class="k">def</span> <span class="nf">translate</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span>
      <span class="k">begin</span>
        <span class="n">original_fallbacks</span> <span class="o">=</span> <span class="no">I18n</span><span class="p">.</span><span class="nf">fallbacks</span>
        <span class="no">I18n</span><span class="p">.</span><span class="nf">fallbacks</span> <span class="o">=</span> <span class="no">I18n</span><span class="p">.</span><span class="nf">available_locales</span><span class="p">.</span><span class="nf">index_with</span> <span class="p">{</span> <span class="o">|</span><span class="n">locale</span><span class="o">|</span> <span class="p">[</span><span class="n">locale</span><span class="p">]</span> <span class="p">}</span>

        <span class="n">ignored</span> <span class="o">=</span> <span class="no">IGNORED_I18N_KEYS</span><span class="p">.</span><span class="nf">any?</span> <span class="p">{</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span> <span class="p">[</span><span class="n">options</span><span class="p">[</span><span class="ss">:scope</span><span class="p">],</span> <span class="n">key</span><span class="p">].</span><span class="nf">compact</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"."</span><span class="p">).</span><span class="nf">start_with?</span><span class="p">(</span><span class="n">k</span><span class="p">)</span> <span class="p">}</span>
        <span class="k">unless</span> <span class="n">ignored</span>
          <span class="n">available_locales</span><span class="p">.</span><span class="nf">without</span><span class="p">(</span><span class="no">I18n</span><span class="p">.</span><span class="nf">locale</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">locale</span><span class="o">|</span>
            <span class="n">with_locale</span><span class="p">(</span><span class="n">locale</span><span class="p">)</span> <span class="p">{</span> <span class="n">original_translate</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">,</span> <span class="ss">raise: </span><span class="kp">true</span><span class="p">)</span> <span class="p">}</span>
          <span class="k">end</span>
        <span class="k">end</span>
      <span class="k">ensure</span>
        <span class="no">I18n</span><span class="p">.</span><span class="nf">fallbacks</span> <span class="o">=</span> <span class="n">original_fallbacks</span>
      <span class="k">end</span>

      <span class="n">original_translate</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This is a fun patch that causes <code>I18n.raise_on_missing_translations</code> to raise on any missing translation across <em>all available locales</em>, not just the current locale.</p>

<h3 id="i18n_tasks--psych-yaml-mangling">
<code>i18n_tasks</code> / Psych YAML mangling</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/i18n_tasks_normalize.rb</span>

<span class="c1"># Drop this at the top of config/i18n_tasks.yml too:</span>
<span class="c1"># &lt;% require "./config/i18n_tasks_normalize" %&gt;</span>

<span class="k">module</span> <span class="nn">I18nTaskYamlExt</span>
  <span class="no">UNMASKED_EMOJI</span> <span class="o">=</span> <span class="sr">/
    (?:
      (?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)   # base emoji
      (?:\u200D(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F))* # + ZWJ parts
    )
  /ux</span>

  <span class="k">def</span> <span class="nf">dump</span><span class="p">(</span><span class="n">tree</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
    <span class="n">builder</span> <span class="o">=</span> <span class="no">Psych</span><span class="o">::</span><span class="no">Visitors</span><span class="o">::</span><span class="no">YAMLTree</span><span class="p">.</span><span class="nf">create</span>
    <span class="n">builder</span> <span class="o">&lt;&lt;</span> <span class="n">tree</span>
    <span class="n">ast</span> <span class="o">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">tree</span>
    <span class="n">_process_node</span><span class="p">(</span><span class="n">ast</span><span class="p">)</span>
    <span class="n">strip_trailing_spaces</span><span class="p">(</span><span class="n">restore_emojis</span><span class="p">(</span><span class="n">ast</span><span class="p">.</span><span class="nf">to_yaml</span><span class="p">(</span><span class="kp">nil</span><span class="p">,</span> <span class="n">options</span> <span class="o">||</span> <span class="p">{})))</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">_process_node</span><span class="p">(</span><span class="n">node</span><span class="p">)</span>
    <span class="k">case</span> <span class="n">node</span>
    <span class="k">when</span> <span class="no">Psych</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">Scalar</span>
      <span class="n">node</span><span class="p">.</span><span class="nf">plain</span> <span class="o">=</span> <span class="kp">false</span>
      <span class="n">node</span><span class="p">.</span><span class="nf">quoted</span> <span class="o">=</span> <span class="kp">true</span>
      <span class="n">node</span><span class="p">.</span><span class="nf">style</span> <span class="o">=</span> <span class="n">node</span><span class="p">.</span><span class="nf">value</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span> <span class="p">?</span> <span class="no">Psych</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">Scalar</span><span class="o">::</span><span class="no">LITERAL</span> <span class="p">:</span> <span class="no">Psych</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">Scalar</span><span class="o">::</span><span class="no">DOUBLE_QUOTED</span> <span class="c1"># &lt;== THE ENTIRE PURPOSE OF THIS MONKEYPATCH 🫠</span>
      <span class="n">node</span><span class="p">.</span><span class="nf">value</span> <span class="o">=</span> <span class="n">_mask_emoji</span><span class="p">(</span><span class="n">node</span><span class="p">.</span><span class="nf">value</span><span class="p">)</span> <span class="k">if</span> <span class="n">node</span><span class="p">.</span><span class="nf">style</span> <span class="o">==</span> <span class="no">Psych</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">Scalar</span><span class="o">::</span><span class="no">LITERAL</span> <span class="c1"># pre-mask emoji because otherwise libyaml will double-quote when we want literal style</span>
    <span class="k">when</span> <span class="no">Psych</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">Mapping</span>
      <span class="c1"># only process the values, not the keys</span>
      <span class="n">node</span><span class="p">.</span><span class="nf">children</span><span class="p">.</span><span class="nf">each_slice</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">_key</span><span class="p">,</span> <span class="n">value</span><span class="o">|</span> <span class="n">_process_node</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="p">}</span>
    <span class="k">when</span> <span class="no">Psych</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">Stream</span><span class="p">,</span> <span class="no">Psych</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">Document</span><span class="p">,</span> <span class="no">Psych</span><span class="o">::</span><span class="no">Nodes</span><span class="o">::</span><span class="no">Sequence</span>
      <span class="n">node</span><span class="p">.</span><span class="nf">children</span><span class="p">.</span><span class="nf">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">node</span><span class="o">|</span> <span class="n">_process_node</span><span class="p">(</span><span class="n">node</span><span class="p">)</span> <span class="p">}</span>
    <span class="k">else</span>
      <span class="k">raise</span> <span class="s2">"not handling </span><span class="si">#{</span><span class="n">node</span><span class="p">.</span><span class="nf">inspect</span><span class="si">}</span><span class="s2">"</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="c1"># libyaml will do this, but we want to do it first so that libyaml doesn't _also_</span>
  <span class="c1"># mark the node as unprintable and thus prevent it from being in a literal</span>
  <span class="c1"># https://github.com/yaml/libyaml/issues/279</span>
  <span class="c1"># "Hello 👋 world 🌍!" =&gt; "Hello \\u0001F44B world \\u0001F30D!"</span>
  <span class="k">def</span> <span class="nf">_mask_emoji</span><span class="p">(</span><span class="n">string</span><span class="p">)</span>
    <span class="n">string</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="no">UNMASKED_EMOJI</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">emoji</span><span class="o">|</span>
      <span class="n">emoji</span><span class="p">.</span><span class="nf">codepoints</span>
        <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">cp</span><span class="o">|</span> <span class="nb">format</span><span class="p">(</span><span class="s1">'\\u%08X'</span><span class="p">,</span> <span class="n">cp</span><span class="p">)</span> <span class="p">}</span>
        <span class="p">.</span><span class="nf">join</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">I18n</span><span class="o">::</span><span class="no">Tasks</span><span class="o">::</span><span class="no">Data</span><span class="o">::</span><span class="no">Adapter</span><span class="o">::</span><span class="no">YamlAdapter</span><span class="p">.</span><span class="nf">singleton_class</span><span class="p">.</span><span class="nf">prepend</span> <span class="no">I18nTaskYamlExt</span>
</code></pre></div></div>

<p>Ok, <code>i18n_tasks</code> is absolutely essential for doing localization. But it works by roundtripping your YAML through Psych.  This patch does 2 things:</p>

<ul>
  <li>It makes Psych output every string as <em>strictly either</em> a doublequoted string or, if it contains a newline, as literal-block (<code>key: |</code>). Without the patch, Psych will swap around single and doublequoted strings and convert literal-blocks to double-quoted strings with <code>\n</code>s</li>
  <li>Emojis don’t get mangled.</li>
</ul>

<h3 id="custom-mailers">Custom mailers</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/mailers.rb</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:action_mailer</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">ActionMailer</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">add_delivery_method</span> <span class="ss">:twilio_sms</span><span class="p">,</span> <span class="no">TwilioSmsDelivery</span>
  <span class="no">ActionMailer</span><span class="o">::</span><span class="no">Base</span><span class="p">.</span><span class="nf">add_delivery_method</span> <span class="ss">:null</span><span class="p">,</span> <span class="no">NullDelivery</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I’m one of the rare true-fans of Action Mailer who wants to deliver messages to <em>more</em> channels via Action Mailer, not none.  Our app uses Action Mailer to construct and deliver SMS messages too via a custom delivery method. I’ve <a href="https://island94.org/2024/10/technical-reflection-on-disaster-relief-assistance-for-immigrants">touched on</a> this previously.</p>

<h3 id="markdown">Markdown</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/markdown.rb</span>

<span class="k">module</span> <span class="nn">Markdown</span>
  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">convert</span><span class="p">(</span><span class="n">text</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">,</span> <span class="o">**</span><span class="n">options</span><span class="p">)</span>
    <span class="k">raise</span> <span class="no">ArgumentError</span><span class="p">,</span> <span class="s2">"Can't provide both text and block"</span> <span class="k">if</span> <span class="n">text</span> <span class="o">&amp;&amp;</span> <span class="nb">block_given?</span>

    <span class="n">text</span> <span class="o">=</span> <span class="k">yield</span> <span class="k">if</span> <span class="nb">block_given?</span>
    <span class="k">return</span> <span class="s2">""</span> <span class="k">unless</span> <span class="n">text</span>

    <span class="n">text</span> <span class="o">=</span> <span class="n">text</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">strip_heredoc</span>
    <span class="n">options</span> <span class="o">=</span> <span class="n">options</span><span class="p">.</span><span class="nf">reverse_merge</span><span class="p">(</span>
      <span class="ss">auto_ids: </span><span class="kp">false</span><span class="p">,</span>
      <span class="ss">smart_quotes: </span><span class="p">[</span><span class="s2">"apos"</span><span class="p">,</span> <span class="s2">"apos"</span><span class="p">,</span> <span class="s2">"quot"</span><span class="p">,</span> <span class="s2">"quot"</span><span class="p">],</span> <span class="c1"># disable smart quotes</span>
      <span class="ss">input: </span><span class="s1">'GFM'</span><span class="p">,</span>
      <span class="ss">hard_wrap: </span><span class="kp">true</span> <span class="c1"># turn single newlines into &lt;br /&gt;</span>
    <span class="p">)</span>
    <span class="no">Kramdown</span><span class="o">::</span><span class="no">Document</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">options</span><span class="p">).</span><span class="nf">to_html</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">inline</span><span class="p">(</span><span class="n">text</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">,</span> <span class="o">**</span><span class="p">)</span>
    <span class="c1"># Custom input parser defined in Kramdown::Parser::Inline</span>
    <span class="n">convert</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="ss">input: </span><span class="s2">"Inline"</span><span class="p">,</span> <span class="o">**</span><span class="p">).</span><span class="nf">strip</span>
  <span class="k">end</span>

  <span class="k">module</span> <span class="nn">HtmlSafeTranslationExt</span>
    <span class="k">def</span> <span class="nf">html_safe_translation_key?</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
      <span class="n">key</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">end_with?</span><span class="p">(</span><span class="s2">"_md"</span><span class="p">)</span> <span class="o">||</span> <span class="k">super</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">module</span> <span class="nn">I18nBackendExt</span>
    <span class="k">def</span> <span class="nf">translate</span><span class="p">(</span><span class="n">locale</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
      <span class="n">result</span> <span class="o">=</span> <span class="k">super</span>
      <span class="c1"># Rails missing key returns as MISSING_TRANSLATION =&gt; (2**60) =&gt; -1152921504606846976</span>
      <span class="k">if</span> <span class="n">key</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">end_with?</span><span class="p">(</span><span class="s2">"_md"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">result</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">String</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">result</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
          <span class="no">Markdown</span><span class="p">.</span><span class="nf">convert</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
        <span class="k">else</span>
          <span class="no">Markdown</span><span class="p">.</span><span class="nf">inline</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
        <span class="k">end</span>
      <span class="k">else</span>
        <span class="n">result</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">HtmlSafeTranslation</span><span class="p">.</span><span class="nf">prepend</span> <span class="no">Markdown</span><span class="o">::</span><span class="no">HtmlSafeTranslationExt</span>
<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:i18n</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">I18n</span><span class="p">.</span><span class="nf">backend</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">prepend</span> <span class="no">Markdown</span><span class="o">::</span><span class="no">I18nBackendExt</span>
<span class="k">end</span>

<span class="c1"># Generate HTML from Markdown without any block-level elements (p, etc.)</span>
<span class="c1"># http://stackoverflow.com/a/30468100/241735</span>
<span class="k">module</span> <span class="nn">Kramdown</span>
  <span class="k">module</span> <span class="nn">Parser</span>
    <span class="k">class</span> <span class="nc">Inline</span> <span class="o">&lt;</span> <span class="no">Kramdown</span><span class="o">::</span><span class="no">Parser</span><span class="o">::</span><span class="no">Kramdown</span>
      <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
        <span class="k">super</span>
        <span class="vi">@block_parsers</span> <span class="o">=</span> <span class="p">[]</span>
      <span class="k">end</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>There’s two features here:</p>

<ul>
  <li>
<code>Markdown.convert(string)</code> and <code>Markdown.inline(string)</code> are simple helpers that turn a string of markdown formatted text into html</li>
  <li>The entirety rest of the code duplicates Rails i18n behavior for html-safing <code>_html</code> keys. Any i18n key that ends in <code>_md</code> will convert the value from markdown to html, and then mark it as html safe. I love authoring complex i18n keys in markdown (especially when <code>i18n_tasks</code>/Psych doesn’t mangle the newlines, see previously).</li>
</ul>

<h3 id="rbtrace"><code>rbtrace</code></h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/rbtrace.rb</span>
<span class="k">if</span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'RBTRACE'</span><span class="p">]</span>
  <span class="nb">require</span> <span class="s1">'rbtrace'</span>

  <span class="vg">$stdout</span><span class="p">.</span><span class="nf">puts</span> <span class="s2">"Enabled rbtrace. Example commands:"</span>
  <span class="vg">$stdout</span><span class="p">.</span><span class="nf">puts</span> <span class="s2">"- Show all method calls: $ bundle exec rbtrace --pid </span><span class="si">#{</span><span class="no">Process</span><span class="p">.</span><span class="nf">pid</span><span class="si">}</span><span class="s2"> --firehose"</span>
  <span class="vg">$stdout</span><span class="p">.</span><span class="nf">puts</span> <span class="s2">"- Debug Rails deadlock: $ bundle exec rbtrace --pid </span><span class="si">#{</span><span class="no">Process</span><span class="p">.</span><span class="nf">pid</span><span class="si">}</span><span class="s2"> --eval </span><span class="se">\"</span><span class="s2">puts output = ActionDispatch::DebugLocks.new(nil).send(:render_details, nil); output</span><span class="se">\"</span><span class="s2">"</span>
  <span class="vg">$stdout</span><span class="p">.</span><span class="nf">puts</span> <span class="o">&lt;&lt;~</span><span class="no">TEXT</span><span class="sh">
    - Show all threads: $ bundle exec rbtrace --pid </span><span class="si">#{</span><span class="no">Process</span><span class="p">.</span><span class="nf">pid</span><span class="si">}</span><span class="sh"> --eval "ObjectSpace.each_object(::Thread).to_a.each { puts(it, it.backtrace_locations, '------') }; nil"
</span><span class="no">  TEXT</span>
  <span class="vg">$stdout</span><span class="p">.</span><span class="nf">puts</span> <span class="s2">"- Heap Dump: $ bundle exec rbtrace --pid </span><span class="si">#{</span><span class="no">Process</span><span class="p">.</span><span class="nf">pid</span><span class="si">}</span><span class="s2"> --eval 'Thread.new{require </span><span class="se">\"</span><span class="s2">objspace</span><span class="se">\"</span><span class="s2">; GC.start; io=File.open(</span><span class="se">\"</span><span class="s2">tmp/ruby-heap.</span><span class="se">\#</span><span class="s2">{Time.now.to_i}.dump</span><span class="se">\"</span><span class="s2">, </span><span class="se">\"</span><span class="s2">w</span><span class="se">\"</span><span class="s2">); ObjectSpace.dump_all(output: io); io.close }'"</span>
  <span class="vg">$stdout</span><span class="p">.</span><span class="nf">puts</span> <span class="s1">'Press Enter to continue...'</span>
  <span class="vg">$stdin</span><span class="p">.</span><span class="nf">gets</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I have a terrible memory for debugging commands. so this is how I document them. I pair this with a <code>bin/productionrails</code> script that allows me to boot a Rails in production-mode locally by setting up all the necessary environment variables with placeholder values, and disabling https and so forth.</p>

<h3 id="sprocket-reproducible-assets">Sprocket Reproducible Assets</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/sprockets.rb</span>

<span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">configure</span> <span class="k">do</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">assets</span><span class="p">.</span><span class="nf">gzip</span> <span class="o">=</span> <span class="kp">false</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Gzipped Sprocket include the build time and thus are uncacheable across builds. I’m waiting for <a href="https://github.com/rails/sprockets/pull/821">someone to look at my PR</a>.🤷</p>

<h3 id="turbo-stream-jobs">Turbo Stream jobs</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/turbo.rb</span>

<span class="no">ActiveSupport</span><span class="p">.</span><span class="nf">on_load</span><span class="p">(</span><span class="ss">:active_job</span><span class="p">)</span> <span class="k">do</span>
  <span class="no">Turbo</span><span class="o">::</span><span class="no">Streams</span><span class="o">::</span><span class="no">BroadcastJob</span><span class="p">.</span><span class="nf">queue_name</span> <span class="o">=</span> <span class="s2">"turbo"</span>
  <span class="no">Turbo</span><span class="o">::</span><span class="no">Streams</span><span class="o">::</span><span class="no">ActionBroadcastJob</span><span class="p">.</span><span class="nf">queue_name</span> <span class="o">=</span> <span class="s2">"turbo"</span>
  <span class="no">Turbo</span><span class="o">::</span><span class="no">Streams</span><span class="o">::</span><span class="no">BroadcastStreamJob</span><span class="p">.</span><span class="nf">queue_name</span> <span class="o">=</span> <span class="s2">"turbo"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Somone should make this a configuration option 🫵</p>

<h3 id="view-components-that-can-broadcast_later">View Components that can <code>broadcast_later</code>
</h3>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/view_component.rb</span>

<span class="c1"># Instantiate a ViewComponents that is (optionally) serializable by Active Job</span>
<span class="c1"># but otherwise behaves like a normal ViewComponent. This allows it to be passed</span>
<span class="c1"># as a renderable into `broadcast_action_later_to`.</span>
<span class="c1">#</span>
<span class="c1"># To use, include the `ViewComponent::Serializable` concern:</span>
<span class="c1">#</span>
<span class="c1">#  class ApplicationComponent &lt; ViewComponent::Base</span>
<span class="c1">#    include ViewComponent::Serializable</span>
<span class="c1">#  end</span>
<span class="c1">#</span>
<span class="c1"># And then call `serializable` instead of `new` when instantiating:</span>
<span class="c1">#</span>
<span class="c1">#   Turbo::StreamsChannel.broadcast_action_later_to(</span>
<span class="c1">#     :admin, client, :messages,</span>
<span class="c1">#     action: :update,</span>
<span class="c1">#     target: ActionView::RecordIdentifier.dom_id(client, :messages),</span>
<span class="c1">#     renderable: MessageComponent.serializable(message: message)</span>
<span class="c1">#   )</span>
<span class="c1">#</span>
<span class="k">module</span> <span class="nn">ViewComponent</span>
  <span class="k">module</span> <span class="nn">Serializable</span>
    <span class="kp">extend</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Concern</span>

    <span class="n">included</span> <span class="k">do</span>
      <span class="nb">attr_reader</span> <span class="ss">:serializable_args</span>
    <span class="k">end</span>

    <span class="n">class_methods</span> <span class="k">do</span>
      <span class="k">def</span> <span class="nf">serializable</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">)</span>
        <span class="n">new</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">).</span><span class="nf">tap</span> <span class="k">do</span> <span class="o">|</span><span class="n">instance</span><span class="o">|</span>
          <span class="n">instance</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="ss">:@serializable_args</span><span class="p">,</span> <span class="n">args</span><span class="p">)</span>
        <span class="k">end</span>
      <span class="k">end</span>
      <span class="n">ruby2_keywords</span><span class="p">(</span><span class="ss">:serializable</span><span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">ViewComponentSerializer</span> <span class="o">&lt;</span> <span class="no">ActiveJob</span><span class="o">::</span><span class="no">Serializers</span><span class="o">::</span><span class="no">ObjectSerializer</span>
  <span class="k">def</span> <span class="nf">klass</span>
    <span class="no">ViewComponent</span><span class="o">::</span><span class="no">Base</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">serialize?</span><span class="p">(</span><span class="n">argument</span><span class="p">)</span>
    <span class="n">argument</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">ViewComponent</span><span class="o">::</span><span class="no">Base</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">argument</span><span class="p">.</span><span class="nf">respond_to?</span><span class="p">(</span><span class="ss">:serializable_args</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">serialize</span><span class="p">(</span><span class="n">view_component</span><span class="p">)</span>
    <span class="k">super</span><span class="p">(</span>
      <span class="s2">"component"</span> <span class="o">=&gt;</span> <span class="n">view_component</span><span class="p">.</span><span class="nf">class</span><span class="p">.</span><span class="nf">name</span><span class="p">,</span>
      <span class="s2">"arguments"</span> <span class="o">=&gt;</span> <span class="no">ActiveJob</span><span class="o">::</span><span class="no">Arguments</span><span class="p">.</span><span class="nf">serialize</span><span class="p">(</span><span class="n">view_component</span><span class="p">.</span><span class="nf">serializable_args</span><span class="p">),</span>
    <span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">deserialize</span><span class="p">(</span><span class="nb">hash</span><span class="p">)</span>
    <span class="nb">hash</span><span class="p">[</span><span class="s2">"component"</span><span class="p">].</span><span class="nf">safe_constantize</span><span class="o">&amp;</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="o">*</span><span class="no">ActiveJob</span><span class="o">::</span><span class="no">Arguments</span><span class="p">.</span><span class="nf">deserialize</span><span class="p">(</span><span class="nb">hash</span><span class="p">[</span><span class="s2">"arguments"</span><span class="p">]))</span>
  <span class="k">end</span>

  <span class="no">ActiveJob</span><span class="o">::</span><span class="no">Serializers</span><span class="p">.</span><span class="nf">add_serializers</span><span class="p">(</span><span class="nb">self</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>I only use View Components in the complex parts of my app, <em>and</em> the complex parts of my app are complex because they do a lot of <a href="https://thoughtbot.com/blog/hotwire-turbo-streaming-viewcomponents">Turbo Streaming and asynchronous updating</a>. I shared this upstream with View Component and it’s happening, though I <a href="https://github.com/ViewComponent/view_component/pull/2595#issuecomment-4201048398">already can imagine ways to do this better</a>.</p>

<h3 id="git-worktree-support">Git Worktree Support</h3>

<p>This set of things is rather sprawling. I have a <a href="https://gist.github.com/bensheldon/f475a2669d72256545df5e2fcd1a4dae"><code>GitWorktree</code> class</a> that fetches the current Git Worktree name, and then uses either the string, or a deterministic integer, to prevent collisions during development and testing (itself with <code>parallel_tests</code>) across multiple worktrees concurrently:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># database.yml</span>
<span class="na">development</span><span class="pi">:</span>
  <span class="na">database</span><span class="pi">:</span> <span class="s">frontdoor_development&lt;%= GitWorktree.db_suffix %&gt;</span>
<span class="na">test</span><span class="pi">:</span>
  <span class="na">development: database</span><span class="pi">:</span> <span class="s">frontdoor_test&lt;%= GitWorktree.db_suffix %&gt;&lt;%= ENV["TEST_ENV_NUMBER"].then { it.present? ? "_#{it}"</span> <span class="err">:</span> <span class="s2">"</span><span class="s">"</span> <span class="err">}</span> <span class="err">%</span><span class="pi">&gt;</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># puma.rb</span>
<span class="n">port</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"PORT"</span><span class="p">,</span> <span class="no">GitWorktree</span><span class="p">.</span><span class="nf">integer</span><span class="p">(</span><span class="mi">3000</span><span class="o">..</span><span class="mi">3999</span><span class="p">))</span>

<span class="c1"># application.rb</span>
<span class="n">config</span><span class="p">.</span><span class="nf">x</span><span class="p">.</span><span class="nf">session_prefix</span> <span class="o">=</span> <span class="s2">"frontdoor"</span>
<span class="k">if</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">env</span><span class="p">.</span><span class="nf">development?</span>
  <span class="n">port</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"PORT"</span><span class="p">,</span> <span class="no">GitWorktree</span><span class="p">.</span><span class="nf">integer</span><span class="p">(</span><span class="mi">3000</span><span class="o">..</span><span class="mi">3999</span><span class="p">))</span>
  <span class="n">config</span><span class="p">.</span><span class="nf">x</span><span class="p">.</span><span class="nf">session_prefix</span> <span class="o">=</span> <span class="s2">"frontdoor_</span><span class="si">#{</span><span class="n">port</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="n">config</span><span class="p">.</span><span class="nf">session_store</span> <span class="ss">:cookie_store</span><span class="p">,</span> <span class="ss">key: </span><span class="s2">"_</span><span class="si">#{</span><span class="n">config</span><span class="p">.</span><span class="nf">x</span><span class="p">.</span><span class="nf">session_prefix</span><span class="si">}</span><span class="s2">_session"</span>

<span class="c1"># spec/support/capybara.rb</span>
<span class="no">Capybara</span><span class="p">.</span><span class="nf">server_port</span> <span class="o">=</span> <span class="no">GitWorktree</span><span class="p">.</span><span class="nf">integer</span><span class="p">(</span><span class="mi">4000</span><span class="o">..</span><span class="mi">4990</span><span class="p">,</span> <span class="ss">stride: </span><span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"PARALLEL_TEST_GROUPS"</span><span class="p">,</span> <span class="mi">1</span><span class="p">).</span><span class="nf">to_i</span><span class="p">)</span> <span class="o">+</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"TEST_ENV_NUMBER"</span><span class="p">,</span> <span class="mi">0</span><span class="p">).</span><span class="nf">to_i</span>
</code></pre></div></div>

          ]]>
        </description>
        <link>https://island94.org/2026/05/one-year-of-ruby-on-rails-configuration</link>
        <guid isPermaLink="true">https://island94.org/2026/05/one-year-of-ruby-on-rails-configuration</guid>
        <pubDate>Thu, 14 May 2026 19:13:00 +0000</pubDate>
      </item>
      <item>
        <title>A bulletproof `wait_for_turbo` test helper</title>
        <description>
          <![CDATA[
          <p>I like Hotwired Turbo like I liked UJS (Unobtrusive Javascript): it adds a few small but helpful tricks to my application’s toolbox. One problem though with adding any javascript is that because the webpage is more dynamic, writing full-browser system tests becomes harder because we need to make sure the webpage is in <em>just the right state</em> to interact with it in tests. It’s annoying to write tests super defensively to avoid flakey tests.</p>

<h3 id="enumerating-states-turbo-can-be-in">Enumerating states Turbo can be in</h3>

<p>Turbo adds some attributes to the webpage DOM that we can look for in our tests to ensure that things are in the right state. But it’s tricky. Let’s start with the trickiest thing first.</p>

<p>Turbo sets <code>aria-busy</code>  when navigation is happening:</p>

<ul>
  <li>Turbo Drive sets <code>html[aria-busy]</code> when navigating</li>
  <li>Turbo Frames set <code>turbo-frame[aria-busy]</code> when navigating</li>
  <li>Turbo forms set <code>form[aria-busy]</code> when submitting</li>
</ul>

<p>…but when doing a very typical Submit-a-Form-and-Redirect operation, Turbo drops the <code>[aria-busy]</code> for a brief moment after the form submit returns a redirect response but before navigating to the redirected page sets the <code>[aria-busy]</code> again. That sucks.</p>

<p>The other things we need to be aware of and wait for are more pedestrian:</p>

<ul>
  <li>The first time a page is loaded and Turbo hasn’t been loaded at all yet. This probably wouldn’t matter because, of course, we’re all using progressive enhancement so nothing is functionally inaccessible when javascript isn’t loaded. But our tests should be deterministic. And we have the non-browser Rack Test driver to cover the non-javascript scenario (which are super fast too). To handle this scenario, we’ll add an attribute to our server-rendered HTML layout, and then remove it after Turbo has loaded.</li>
  <li>Turbo Previews. Which is like a weird cached version of the page Turbo will show, but might get yanked away when the <em>actual</em> response finishes and Turbo swaps them. Fortunately turbo adds a <code>data-turbo-preview</code> attribute we can test for.</li>
</ul>

<h3 id="doing-the-simplest-thing-about-it">Doing the simplest thing about it</h3>

<p>Let’s start with the test helper, and then we’ll wire up the pieces:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">wait_for_turbo</span>
  <span class="k">return</span> <span class="k">unless</span> <span class="no">Capybara</span><span class="p">.</span><span class="nf">current_driver</span> <span class="o">!=</span> <span class="ss">:rack_test</span>

  <span class="n">page</span><span class="p">.</span><span class="nf">assert_no_selector</span> <span class="s2">"html[aria-busy], form[aria-busy], turbo-frame[aria-busy], html[data-turbo-not-loaded], html[data-turbo-loading], html[data-turbo-preview]"</span><span class="p">,</span> <span class="ss">visible: :all</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Not bad! It simply waits, because <code>assert_no_selector</code> uses Capybara’s async timeout, until the page doesn’t have any of the “I’m loading” attributes on it. Easy.</p>

<p>To handle the <code>turbo-not-loaded</code> attribute, add something like this to your layout(s):</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&lt;!--</span> <span class="n">app</span><span class="o">/</span><span class="n">views</span><span class="o">/</span><span class="n">layouts</span><span class="o">/</span><span class="n">application</span><span class="p">.</span><span class="nf">html</span><span class="p">.</span><span class="nf">erb</span> <span class="o">--&gt;</span>
<span class="o">&lt;</span><span class="n">html</span> <span class="n">data</span><span class="o">-</span><span class="n">turbo</span><span class="o">-</span><span class="ow">not</span><span class="o">-</span><span class="n">loaded</span><span class="o">=</span><span class="s2">"1"</span><span class="o">&gt;</span>
</code></pre></div></div>

<p>And then in your javascript, at the very bottom after you’ve loaded Turbo and everything else:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/javascript/application.js</span>

<span class="k">import</span> <span class="dl">"</span><span class="s2">@hotwired/turbo-rails</span><span class="dl">"</span>
<span class="c1">// ...</span>

<span class="nb">document</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbo:load</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nf">removeAttribute</span><span class="p">(</span><span class="dl">"</span><span class="s2">data-turbo-not-loaded</span><span class="dl">"</span><span class="p">)</span>
  <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nf">removeAttribute</span><span class="p">(</span><span class="dl">"</span><span class="s2">data-turbo-loading</span><span class="dl">"</span><span class="p">)</span>
<span class="p">})</span>

<span class="nb">document</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbo:submit-start</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nf">closest</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbo-frame</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nf">setAttribute</span><span class="p">(</span><span class="dl">"</span><span class="s2">data-turbo-loading</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">1</span><span class="dl">"</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">})</span>
<span class="nb">document</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbo:submit-end</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
   <span class="k">if </span><span class="p">(</span><span class="o">!</span><span class="nx">event</span><span class="p">.</span><span class="nx">detail</span><span class="p">.</span><span class="nx">fetchResponse</span><span class="p">?.</span><span class="nx">redirected</span><span class="p">)</span> <span class="p">{</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">.</span><span class="nf">removeAttribute</span><span class="p">(</span><span class="dl">"</span><span class="s2">data-turbo-loading</span><span class="dl">"</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<p>The carve-out logic for setting <code>data-turbo-loading</code> on <code>turbo:submit-start</code> and removing it on <code>turbo:submit-end</code> is a bit complicated:</p>

<ul>
  <li>We don’t want to set <code>data-turbo-loading</code> on Turbo Frames because Turbo Frames don’t emit <code>turbo:load</code> events so the attribute would never be removed. More importantly and fortunately, Turbo Frames don’t suffer from an <code>aria-busy</code> gap.</li>
  <li>And we don’t want to remove <code>data-turbo-loading</code> if it’s a redirect, to bridge the <code>aria-busy</code> gap mentioned earlier. We’ll wait for the subsequent <code>turbo:load</code> to clean it up.</li>
</ul>

<p>There it is. Now you just need to put <code>wait_for_turbo</code> before any action that could potentially engage Turbo e.g.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ...</span>
<span class="n">click_on</span> <span class="s2">"Do something"</span>
<span class="n">wait_for_turbo</span>
<span class="n">click_on</span> <span class="s2">"The result"</span>
<span class="c1">#...</span>
</code></pre></div></div>

<h3 id="i-hate-it">I hate it</h3>

<p>Yeah, that’s awful. We want our system tests to invoke declarative behavior, not be littered with “wait for it….”. So… we’re going to patch Capybara. Here we go…:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="nn">SystemTurboClick</span>
  <span class="k">def</span> <span class="nf">click</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
    <span class="k">unless</span> <span class="n">session</span><span class="p">.</span><span class="nf">driver</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Capybara</span><span class="o">::</span><span class="no">RackTest</span><span class="o">::</span><span class="no">Driver</span><span class="p">)</span>
      <span class="n">assert_no_ancestor</span><span class="p">(</span><span class="s2">"[aria-busy], html[data-turbo-not-loaded], html[data-turbo-loading], html[data-turbo-preview]"</span><span class="p">,</span> <span class="ss">visible: :all</span><span class="p">)</span>
    <span class="k">end</span>
    <span class="k">super</span>
  <span class="k">end</span>
<span class="k">end</span>

<span class="no">Capybara</span><span class="o">::</span><span class="no">Node</span><span class="o">::</span><span class="no">Element</span><span class="p">.</span><span class="nf">prepend</span> <span class="no">SystemTurboClick</span>
</code></pre></div></div>

<p>Now anytime that Capybara does a click event, it will first wait for Turbo to do its thing.</p>

          ]]>
        </description>
        <link>https://island94.org/2026/03/a-bulletproof-wait_for_turbo-test-helper</link>
        <guid isPermaLink="true">https://island94.org/2026/03/a-bulletproof-wait_for_turbo-test-helper</guid>
        <pubDate>Fri, 06 Mar 2026 00:18:00 +0000</pubDate>
      </item>
      <item>
        <title>War</title>
        <description>
          <![CDATA[
          <p>The second Iraq War started during Spring Break of my sophomore year of university. It was coming. The one international relations class I have ever taken, an honors seminar on US interventionism, had wrapped up several weeks prematurely. The classes’s teacher, Ambassador Barbara Bodine, had been recalled to Washington.</p>

<p>I spent that Spring Break week on campus alone with my thoughts, as <em>any</em> romantic 20-year old I imagine would be. The woman I had just begun dating, who I had met in that same university class, was traveling in Germany. That relationship would last another three years; the Iraq war would last eight.</p>

<p>I did not sign up. My best friend had been exercising with the ROTC cadets. I remember him explaining that the army’s current slogan “Army of ONE” was terrible. He not sign his paperwork. He lives in Germany now.</p>

<p>Another member of our same high school friend group did serve. While on leave later that year, he would kill a man outside his hotel, presumed to have been caught stealing from his car, and then would kill himself.</p>

<p>My grandfather served in the Army, in Alaska, during World War 2. The family lore is that while courting my grandmother, he briefly snuck off base for a date. My grandfather being the model of a loyal, hardworking, reliable private, caused the base commander to exclaim: “AWOL? Not Jerome!”</p>

<p>My other grandfather served during the last days of World War One in his native Hungary, before Trianon. He would go to Spain as an American communist to report on the civil war against fascism. He came back a liberal anti-communist and <a href="https://americancommissar.wordpress.com/">wrote a book about it</a>. The US government would later prevent him from returning to Hungary to cover the revolution against the Soviets for Newsweek.</p>

<p>My dad fell off a cable car as a teenager and messed up his knees. That led to a draft deferment. His knees still cause him pain.</p>

<p>My uncle was drafted and served in Vietnam. We don’t talk about it.</p>

<p>I can check online to see that yes, I did register for Selective Service a week after my 18th birthday.</p>

<p>My father-in-law served in the US Army in Germany after Korea and before Vietnam, paving the way for his US citizenship. His father served in the Chaco War, which would kill 2% of the population of his home nation, Bolivia.</p>

<p>When I was 5, we hosted a family from the Soviet Union through MEND, Mothers Embracing Nuclear Disarmament. One of their gifts was a matryoshka nesting doll. It sits on my piano today.</p>

<p>During my last year working at UMass Boston’s College of Public and Community Service, 2011-ish, I started taking my lunch on a newly-installed bench overlooking Dorchester Bay. The bench had a plaque remembering a US soldier killed in Kandahar Province, Afghanistan.</p>

<p>In the acknowledgements of Spencer Ackerman’s book, <em>Reign of Terror</em>, he writes:</p>

<blockquote>
  <p>Yes, I know this book is incomplete.</p>

  <p>There’s not enough in <em>Reign</em> about the pivotal role of the media in manufacturing consent for the War on Terror. There’s nowhere <em>near</em> enough about the economic forces driving the war, both specific and structural. Ultimately, I found that attempting either analysis swallowed the actual narrative events of the war and <em>still</em> felt superficial. These subjects require books in their own right—particularly one I’m tossing around in my head called <em>Capitalism and Terrorism.</em> (That book is the forum to go into the U.S.–Saudi relationship and its place in all this, I found . . . after failing to get it into <em>Reign</em> satisfactorily.) I chose to attempt to do one thing well instead of three things badly.</p>

  <p>That leads to the second way <em>Reign</em> is incomplete. I had to make a lot of cuts. They were a function of <em>Reign</em>’s particular critique, as well as the unavoidable reality that I have a twenty-year palette of events and a contractually stipulated word limit. (Someone have me on their podcast to talk about Michelle Malkin bullying Dunkin’ Donuts because she was mad Rachael Ray appeared in a commercial wearing a scarf that looked kind of like a kaffiyeh.) Even after all those cuts, at all times writing this book, a voice in my head objects that I’m presenting the “We Didn’t Start the Fire” version of the Forever War. So if I didn’t dwell long enough on events you think are crucial, know that I tried, and please research and write your own versions that outdo mine.</p>

  <p>The third way <em>Reign</em> is incomplete is that much of what I write about remains an official secret. The façade of the War on Terror has been cracked, but we won’t know what the war truly was for decades. I frankly don’t think anything we discover will undermine the critique of <em>Reign</em>. I expect what we learn to reinforce my critique. Consider that at least eighteen hundred photographs of military torture have been barred from public release after a yearslong legal battle. Not even Dan Jones’s torture report could tell the story of CIA renditions, operations that we know applied to more people than the CIA directly jailed and tortured. Some aspects of the War on Terror are likely lost to history forever.</p>
</blockquote>

          ]]>
        </description>
        <link>https://island94.org/2026/03/war</link>
        <guid isPermaLink="true">https://island94.org/2026/03/war</guid>
        <pubDate>Wed, 04 Mar 2026 15:18:00 +0000</pubDate>
      </item>
      <item>
        <title>Post your Prompts</title>
        <description>
          <![CDATA[
          <p>This post is mostly a reaction and reflection on Jon Sully’s <a href="https://jonsully.net/blog/generative-ai-tech-writing-disclose">“Generative AI and Tech Writing: The Right Answer is to Disclose.”</a> Strong agree! I wanted to zoom in on one part of Jon’s post: “<strong>Just Give Me the Prompt”</strong></p>

<p>Seriously. If you are blogging, a prompt is good enough. It’s great! If you’ve got a thought or idea that you want to share with the world, just post that idea. It’s enough.</p>

<p>Jon’s post caused me to reflect on my own writing process. Over the course of the nearly 20 years (!) I’ve been writing here, I’ve become much more comfortable incorporating my initial “gosh, I have this thing to say in my head and I want to get it out”-compulsion into the the thing I evetually write. Just spit it out.</p>

<p>These abstracts statements are more “I want to tell you about …” and less “I want to write a longform blog post that tells you about…”</p>

<ul>
  <li>I want to tell you about this thing I’m proud of, and make you be proud of it too.</li>
  <li>I want to tell you about the shape of a thing I see, so I can ask if you see it too.</li>
  <li>I want to tell you a story in the canon of my life, and I want you to understand why it’s meaningful to me rather than simply dumb or sad or cruel.</li>
</ul>

<p>The form doesn’t matter <em>to the want</em>. The da-dum da-dum rhythm of heading and sentences and commas that results is just how I’ve come to spit out my written words. Don’t worry about that. Post your want.</p>

<p>What I mean to say is: this is working for me; I recommend you try it.</p>

<h3 id="becoming-aware">Becoming aware</h3>

<p>I guess this is a post about writing. When you want to become a better writer, you get told to read more. I don’t think that’s quite right, or not the point I’d emphasize. Instead I’d say: When you want to write better, you should pay more attention when you’re reading to whether what you’re reading works for you and then pause a beat, and become aware of that feeling and, momentarily or however long you want, try to figure out why. …or why not.</p>

<p>Sure, self-awareness is a skill that develops alongside reading more, but I don’t see the point to recommend reading more if you aren’t also becoming more aware of whether you <em>like it</em> or are responding to what you’re reading.</p>

<p>That sort of awareness can be hard because the vast majority of what shows up in front of me is intended to be informational. Work memos, and news reports, and the like. It’s not intended to hit or feel, and usually is intended specifically <em>not</em> to make me feel. But I can ask myself: was this boring-ass memo more or less effective than those other boring-ass memo at achieving its purpose? And why. And recognizing that the purpose isn’t clear at all is a feel too. What is this shit?</p>

<p>…and on that last negative reaction: I personally find it harder to split the mildy good writing from the mildy bad writing. Really good and really bad is obvious, but also rare; most stuff is mid. A mild disgust or boredom or frustration is easier for me to identify than mild understanding or mild respect. “Oh, I wasn’t aware of that before and I now I am” or “oh, this person knows what they’re talking about in an everyday way” is harder for me to sus than “huh?” and “what?” and “ok, whatever that was”</p>

<p>Mildly good itself is a totally reasonable target. I should be so lucky to be surrounded by and produce myself mildly good writing. Mildly good!</p>

<h3 id="my-answer-for-mildly-good">My answer for mildly good</h3>

<p>So, the entire previous section was me recommending that you develop your own personal sense of awareness and taste for writing that works for you.</p>

<p>This section is where I tell you what works for me: <strong>Specificity and economy.</strong> Say the things themselves, and do it directly without a lot of leadup or padding out.</p>

<ul>
  <li>
<strong>“Specificity is the soul of narrative”</strong>, <a href="https://maximumfun.org/episodes/judge-john-hodgman/episode-738-i-was-dreaming-when-i-wrote-this-so-sue-me-if-i-judge-too-fast-live-in-portland-me/">attributed to John Hodman</a> via <a href="https://chris.molanphy.com/why-olivia-rodrigos-drivers-license-had-the-biggest-debut-since-wap/">“Why Olivia Rodrigo’s “Drivers License” Had the Biggest Debut Since “WAP”</a>.</li>
  <li>
<strong>“If I had more time, I would have written a shorter letter.”</strong> <a href="https://quoteinvestigator.com/2012/04/28/shorter-letter/">No one knows</a> where that one comes from, but it’s true: writing clearly, directly, and efficiently is the hard part of writing well.</li>
</ul>

<p>Because this is the section where I laundry list the things I <em>really</em> want to share. And because everything else was a leadup to explain why you might spend time reading it… here is a personal example. This is the kind of thing my dad posts, all the time, on Facebook. It’s mildly perfect.</p>

<blockquote>
  <p>Yesterday morning Constance and I watched the funeral of Prince Philip starting at 6:30am.  She has this button of him, and I have this souvenir of the Royal Visit to Whitehorse in 1959.  My father was to cover it for Newsweek.  He got press credentials for both of us from Ottawa.  I was to be his assistant.  I was 12 years old.</p>

  <p>We flew down to Whitehorse from Fairbanks on a Lions Club charter flight.  We were at the airport for the arrival.  The press plane came first, and then the plane with Queen Elizabeth and Prince Philip.  The stairs were all folded up inside the front door of the plane.  They were deployed and the Queen and Prince Philip descended.  A First Nations (that phrase was not used then) girl gave the Queen some flowers, and local dignataries were presented.  By now the press were pretty jaded, and they just stood and watched.  Other noteworthy events were the ride on the White Pass &amp; Yukon train and the church service.  After a visit to the McBride Museum the Royal Couple boarded the train and stood on the rear platform, waving to the crowded.  They were talking to each other out of the corner of their mouths.  The press boarded and the train departed.  The royal party was in the last car and the press were in the leading 3 or 4 cars.  We stopped at a cross road outside of town and Queen Elizabeth and Prince Philip got into a waiting car to go back into town.</p>

  <p>The train continued on to Carcross, where there was an open bar to the press.  Someone offered to take my father and me for a boat ride on the lake.  We did that.  On another day there was to be a trip to Dawson City.  The Queen did not feel well and only Philip went.  When he returned he was piloting, and landed the plane.  There were episodes on “The Crown” where he was learning to fly.  When they returned to London it was announced she was pregnant.  That was Prince Andrew.</p>

  <p>The other event I remember was the service at the log church.  There were a lot of Canadian army officers standing around in their uniforms with all their medals.  There were some with service medals going back to George V.  For some reason I crossed a security line.  An army officer stopped me, and I pointed to my badge.  He studied  it, and said “I don’t know why they are giving press credentials to 12 year olds now.”  He let me through.</p>
</blockquote>

<p>Detail, detail, detail, and a joke at the end. It’s arguably a shaggy dog story leading up to the humor, but it works because there isn’t extraneous structure or padding or filler. In the comments, my dad <a href="https://www.britishpathe.com/asset/196404/">even found a film archive</a> that has the two of them in it (at 8:54). It means something.</p>

<p><img src="https://island94.org/uploads/2026/whitehorse.jpg" alt=""></p>

<h3 id="back-to-the-prompt">Back to the prompt</h3>

<p>I want to read what is personally meaningful to you. At a minimum I’m curious, and more often, <em>I care.</em> I wouldn’t bother reading it otherwise.</p>

<p>So don’t fuck around having an LLM wrap what you intend to say in meaninglesless and next-token statistical noise. <a href="https://island94.org/2010/06/fierce-editing">Go at it with a ruthless knife.</a> Say what you want to say. Write it down. Tell me. I’ll put it in my RSS reader.</p>

<p>And thank you Jon for writing the <a href="https://jonsully.net/blog/generative-ai-tech-writing-disclose">much longer, more thoughtful piece</a> that inspired this, and the discussion in the Ruby on Rails Performance Slack where I mostly spat all this out already.</p>

          ]]>
        </description>
        <link>https://island94.org/2026/02/post-your-prompts</link>
        <guid isPermaLink="true">https://island94.org/2026/02/post-your-prompts</guid>
        <pubDate>Fri, 27 Feb 2026 12:00:00 +0000</pubDate>
      </item>
      <item>
        <title>Frontier novelty</title>
        <description>
          <![CDATA[
          <p>From Benji Edward’s <a href="https://arstechnica.com/information-technology/2026/01/10-things-i-learned-from-burning-myself-out-with-ai-coding-agents/">“10 things I learned from burning myself out with AI coding agents”</a> describing the challenge of maintaining novelty during AI coding:</p>

<blockquote>
  <p>Due to what might poetically be called “preconceived notions” baked into a coding model’s neural network (more technically, statistical semantic associations), it can be difficult to get AI agents to create truly novel things, even if you carefully spell out what you want.<br>
For example, I spent four days trying to get Claude Code to create an Atari 800 version of my HTML game <em>Violent Checkers</em>, but it had trouble because in the game’s design, the squares on the checkerboard don’t matter beyond their starting positions. No matter how many times I told the agent (and made notes in my Claude project files), it would come back to trying to center the pieces to the squares, snap them within squares, or use the squares as a logical basis of the game’s calculations when they should really just form a background image.</p>

  <p>To get around this in the Atari 800 version, I started over and told Claude that I was creating a game with a UFO (instead of a circular checker piece) flying over a field of adjacent squares—never once mentioning the words “checker,” “checkerboard,” or “checkers.” With that approach, I got the results I wanted.</p>
</blockquote>

<p>I’ve experienced something similar in my own AI-assisted game design. In my case, it’s my perennial desire to have a Simcity-like city builder that operates on “parcels” rather than tiles or zones and, oh boy, the LLM really chafes at it. Is it weird <em>enough</em> to work?</p>

          ]]>
        </description>
        <link>https://island94.org/2026/01/frontier-novelty</link>
        <guid isPermaLink="true">https://island94.org/2026/01/frontier-novelty</guid>
        <pubDate>Sun, 25 Jan 2026 20:08:00 +0000</pubDate>
      </item>
      <item>
        <title>GoodJob, Solid Queue, Sidekiq, Active Job, in 2026</title>
        <description>
          <![CDATA[
          <p>Hey, I’m Ben, the author of GoodJob. Last year at RailsConf I hosted a panel discussion between myself, Solid Queue’s Rosa Gutiérrez, Sidekiq’s Mike Perham, and Karafka and Shoryuken’s Maciej Mensfield called “The Past Present and Future of Background Jobs”. You can <a href="https://www.rubyevents.org/talks/panel-the-past-present-and-future-of-background-jobs">watch that video here</a>.</p>

<p>In this post, I’m writing my personal perspective on how you, dear developer, might decide what background job backend to choose for Rails and Active Job. But everything in context. And oh boy, it’s all context.</p>

<h3 id="a-sober-look-at-how-technical-decisions-are-actually-made">A sober look at how technical decisions are actually made</h3>

<p>I’ve worked in software engineering a long time and seen a lot of bullshit justifications for… doing stuff.</p>

<ul>
  <li>It’s “more modern”, meaning newer, I guess.</li>
  <li>It has more features, or a unique architecture.</li>
  <li>
<em>Everybody</em>’s doing it. Or maybe, this particular <em>someone</em> is doing it. Or <em>nobody</em> is doing it, except for the successful ones.</li>
</ul>

<p>All of this has some flavor of: I want to be different than everyody else, but also the same as the people who are succeeding.</p>

<p>There’s an assumption that the generation of knowledge is <em>efficient</em>. Meaning that all possible problems and solutions have been rigorously measured and stress-tested and their upsides and downsides unearthed; that people of equivalent smarts have spent equivalent time running out equivalent unflawed methodologies and sharing the results across equivalent channels. And our work is to place that preexisting knowledge on the platters of a scale and note which way it tips.</p>

<p>Also true: familiarity breeds contempt. The grass is always greener. And despite things being inherently complex, where the goal should be less “work your way out of the job” and more “don’t work yourself into an early grave”, your managers—whether actual people or your own intrusive thoughts—will not accept that.</p>

<p>Alternatively, I know, from such evergreen classics like Glass’s <em>Facts and Fallacies of Software Engineering</em> and Weinberg’s <em>The Psychology of Computer Programming,</em> it’s rare to deeply track our own technical and operational bottlenecks, the full business contexts, the skills and resources of the team and the broader talent market, and integrate those details into the decision-making process. Most folks start with the solution they want, and work backwards in their argumentation through whatever technical decision process their engineering organization requires. No one shows their work ’cause there’s no work to show; they read the assignment and the answer just popped out.</p>

<p>But not you, of course.</p>

<p>There’s another dimension to this that I think is really important to note: <strong>Decision making for new projects is vastly different than existing projects.</strong></p>

<ul>
  <li>With <strong>New Projects</strong>, the decisions don’t really deeply matter cause nobody knows how it’s going to go and everything is speculative. But also because of bikeshedding and the Law of Triviality those decisions become the most forcefully argued over. It’s really hard for a room of technical people to plainly say they just want to do it one way cause they know it, or try something different for the hell of it and then figure out an effective social compromise instead of debating to death some hardboiled technical justification.</li>
  <li>With <strong>Existing Projects</strong>, you have some data and some context and continuity to base your needs and your decisions on. But there is also an inexorable pressure to <em>do something different</em> and you end up picking a solution-at-hand rather than more deeply digging into a problem area where the solutions are more elusive in the sense of “I’m searching for my keys under the streetlight not because I lost them nearby but because the light is better”.</li>
</ul>

<p>Bringing it back to Rails and Active Job, let’s look at a really big differentiator.</p>

<h3 id="omakase">Omakase</h3>

<p>Solid Queue is a default gem in Rails. If you run <code>rails new</code>, you get Solid Queue. That’s a strong signal that it will work, and that the Rails team confidently stands behind it as much as anything else in the Rails framework. Solid Queue is Ruby on Rails’ chefs’ choice made for you; it’s omakase.</p>

<p>“Eliminate valueless choices” is a powerful principle, as part of the Rails Manifesto. Solid Queue is a good choice, <em>especially</em> if you don’t have to choose. This alone is an incredibly powerful reason to Solid Queue. <strong>You probably chose Rails for the lore.</strong> There’s a manifesto. And it’s actually good. Conceptual integrity is important. Make the center hold.</p>

<p>But if you’ve been following the Rails discourse for a while, you’ll know not everyone agrees that omakase is <em>best</em>, or best for them. Lots of people quietly (and loudly!) go their own way. <em>Callbacks are the worst,</em> they comment underneath every single Reddit post. <em>Nobody actually uses ActionMailer. Why isn’t there a Services directory?  That pattern encourages methods that are too long! All those Concerns, are you for real? Nobody does it that way anymore. It’s not modern.</em></p>

<p>More recently 37 Signals has started releasing source code for several of their applications, and yes, they’re for real and yes, they do still do it that way. But Rails is a big framework (“Batteries included”) and you don’t have to use everything (“No one paradigm”). You can swap something out and remain lore canon.</p>

<p>And really, Solid Queue slots into a real sweet spot. It’s both <em>new</em> and it’s the default. Solid Queue can occupy the unicorn stall that’s both excitingly cutting edge and boringly conservative all at once. This will be a different discussion 5 years from now even if nothing has changed other than the year on the calendar.</p>

<p>But, maybe you do <em>need</em> to order off the menu! And we should dig into that.</p>

<h3 id="incomparables">Incomparables</h3>

<p>Given the infinite rainbow of the human experience, there has to be somebody who does not collapse their categories. I have yet to meet them.</p>

<p>People want a simple decision tree that starts with “Postgres, MySQL, SQLite or Redis?” It’s not so simple.</p>

<ul>
  <li>Postgres on Heroku? Digital Ocean? Neon? Fly’s umpteenth attempt at it? Self-managed? How? Co-located?</li>
  <li>MySQL? MariaDB? Behind Vitess? Planetscale?</li>
  <li>SQLite? Litestream? Did you apply the <em>good</em> patches? How’s your IO?</li>
  <li>Redis? HA? How? You didn’t choose “Flex” did you?</li>
</ul>

<p>And any one of these can have the DBA who swears by that one deep-cut config and won’t budge. And while there is wisdom in sinking engineer-years into completely rearchitecting your system to sidestep a problematic technical peer—oh, I’ve done it—there’s no way you’re gonna write those words in the justification doc. You’re slapping down “more modern” and your junior (or junior-minded) colleague is tweeting “more modern” and HackerNews will cite that a decade from now.</p>

<p>Knowledge is not efficient. You’ll always find another subfloor and several sheer walls of complexity below and beyond whichever one you just pried open. This stuff is not enumerable, so any <em>good</em> decision tree you’ll receive will, on its surface, appear to be a gross and useless simplification despite the experience that went into it.</p>

<h3 id="in-conclusion">In conclusion</h3>

<p>As we’ve deeply explored together, you find yourself in one of two scenarios regarding background jobs:</p>

<ul>
  <li>The backend doesn’t matter. Pick whatever, or pick nothing and use the default. Give whatever nonsense justification you want that serves you best. No shame, all respect if you can recognize this for yourself. You can stop reading now (I know, we love the lore).</li>
  <li>It does actually matter, a lot. And successfully navigating this will be the crowning achievement of your career.</li>
</ul>

<p>If you, dear developer, find yourself in the second bucket, here’s my advice.</p>

<ul>
  <li>
<strong>If performance matters, like actually matters, use Sidekiq Enterprise or Karafka.</strong> <em>But it’s complicated</em>, you comment. <em>It costs money. No one uses it anymore. It’s not modern.</em> Shush. Sidekiq Enterprise and Karafka are, hands down the best for performance outside of building it in-house (a legit choice we won’t explore here). You can make it complicated by trying to precisely nail down what is meant by “performance” (throughput, latency, resources, spiky, steady, whatever), but if you use the word superlatively–“most performant”–your choice is made.</li>
  <li>
<strong>Otherwise:</strong>
    <ul>
      <li>
<strong>If you’re using MySQL or SQLite, use SolidQueue.</strong> it’s your best option for those databases, and it’s really good in an absolute sense too. And it’s getting even better. it should have GoodJob-esque batches soon. No shade, all love.</li>
      <li>
<strong>If you’re using Postgres, use GoodJob.</strong> It’s full featured, and those features are implemented simple-like because they use Postgres-only features; there’s no workaround complexity for other databases. Want to break all the rules of encapsulation and fiddle with the job data? It’s one table. FANG uses GoodJob. It’s modern.</li>
    </ul>
  </li>
</ul>

<p>Because I’m close to GoodJob (familiarity!) I know the limitations too. You’ll have to go around PgBouncer. One guy profiled Postgres several years ago and pointed out where in the performance stratosphere Advisory Locks fall apart. It’s all true.</p>

<p>And there is a lot of relational-database arcana that I don’t think GoodJob nor Solid Queue have documented that you <em>could</em> do. Like vacuuming your tuples and stuff like that. I don’t do it, but some people swear by it. Every system falls apart if you run it hot or weird for long enough, and relational databases are no different than Redis or Kafka.</p>

<p>I <em>could</em> laundry-list any number of downsides and problems people have had with GoodJob… and you probably won’t experience them. Rosa and Mike and Maciej can tell you the same about their own backends. If and when you do experience something bad, it’s likely you’d experience something else bad on any other job backend <em>and then you’d have to work through the problem</em> and it’s likely that problem is unique to you, either because of your unique setup, or a trivial, personal failure to have simply googled it earlier.</p>

<p>And to solve that problem, you’ll probably do some research. And ideally you have a strong community of peers that think like you think, and work on similar systems to the one you work on, and you talk to them and sometimes help solve their problems too. They won’t have <em>your</em> specific answer, but they’ll have some suggestions about what to look at and what to try looking at or googling for you, and <em>maybe</em> the change you make is to change your Active Job backend, but it probably isn’t.</p>

<p>So the takeaway here is that today in 2026 there are a small number of really, really good Active Job backends to choose from, and the most important consideration is that you make friends along the way.</p>

          ]]>
        </description>
        <link>https://island94.org/2026/01/goodjob-solid-queue-sidekiq-active-job-in-2026</link>
        <guid isPermaLink="true">https://island94.org/2026/01/goodjob-solid-queue-sidekiq-active-job-in-2026</guid>
        <pubDate>Fri, 16 Jan 2026 15:26:00 +0000</pubDate>
      </item>
  </channel>
</rss>
