<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ernesto Tagwerker (he/him)</title>
    <description>The latest articles on DEV Community by Ernesto Tagwerker (he/him) (@etagwerker).</description>
    <link>https://dev.to/etagwerker</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F69379%2F614aefdc-d82c-47eb-ba89-134cfa351a69.jpeg</url>
      <title>DEV Community: Ernesto Tagwerker (he/him)</title>
      <link>https://dev.to/etagwerker</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/etagwerker"/>
    <language>en</language>
    <item>
      <title>What We Check Before Migrating Off Heroku</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Tue, 03 Nov 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/fastruby/what-we-check-before-migrating-off-heroku-1d4e</link>
      <guid>https://dev.to/fastruby/what-we-check-before-migrating-off-heroku-1d4e</guid>
      <description>&lt;p&gt;We recently moved a client’s Ruby on Rails app off Heroku: a Rails API with Sidekiq workers and several services around it. Before we changed anything, we spent about a week on discovery. We had a kickoff call with the client’s team on a Monday, and then two of us paired for the rest of the week to map the infrastructure and draft a migration plan.&lt;/p&gt;

&lt;p&gt;Nothing about that app followed one pattern. Its services were deployed in different ways, and some of the things that mattered most for the migration, like the tools outside Heroku that read from the database and the service that promoted code to production, didn’t show up in the Heroku dashboard at all.&lt;/p&gt;

&lt;p&gt;In this article, you will learn what we inventory before moving an app off Heroku, where to look beyond what Heroku shows you, and what our own discovery missed. Almost none of it depends on where you are going: it applies whether your target is Render, &lt;a href="https://www.fastruby.io/blog/deploying-rails-on-fly" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt;, AWS, or something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the people, then verify
&lt;/h2&gt;

&lt;p&gt;We started with a kickoff call where the client’s team gave us an overview of the pieces: the main app, the services around it, and the tools they relied on. It was a high-level conversation, and after it the details were up to us. We went through each repo and each dyno on Heroku ourselves to find out what actually runs where. People describe what they think about every day, and the pieces they don’t think about are often the ones that break a migration.&lt;/p&gt;

&lt;p&gt;Some of what you need to know, Heroku can’t tell you. We worked out part of it ourselves and asked the client’s team about the rest. Before planning any downtime, it helps to know who the customers are, where they are located, and whether there are known peak events, so you can pick a window that will hurt the least.&lt;/p&gt;

&lt;p&gt;Who wrote what is something you can mostly find out on your own: we looked through the commit history of the repos to see who had worked on which part, so we knew who to send questions to later. Why the architecture looks the way it does is a question only the team can answer. Some choices are constraints you need to keep, and others are leftovers you can drop, and you want to know which is which before you rebuild them somewhere else.&lt;/p&gt;

&lt;p&gt;It’s also worth asking whether the app has a maintenance page, and which jobs run on a schedule on staging and on production, since those keep running during a cutover unless someone pauses or moves them.&lt;/p&gt;

&lt;p&gt;A few third-party tools also came up, and it wasn’t obvious at first how each one connected to the app. We ended up sorting every tool that came up into three groups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tools that run on Heroku, which we move.&lt;/li&gt;
&lt;li&gt;Tools that run elsewhere but depend on something we move, which the client’s team updates.&lt;/li&gt;
&lt;li&gt;Tools the migration doesn’t touch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second group needs the most attention, because those tools stop working if nobody updates them after the move. For us, that was an ETL sync and an AI code-review bot, both reading from the database. The client’s team wanted to update them themselves, which was fine, but we still had to find out how each one connected so everyone knew who owned what before cutover day.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Heroku tells you
&lt;/h2&gt;

&lt;p&gt;Once we knew what to look for, we went through every app in the Heroku dashboard, on both staging and production: the add-ons attached to each one, its config vars, and its dynos with their process types and sizes.&lt;/p&gt;

&lt;p&gt;During discovery, we only listed the config vars. They turned into an audit later, when we were getting ready to move each service. For each variable, we searched the codebase to see whether anything still used it, and confirmed with the client’s team when the code didn’t make it clear. Only the variables that were still in use had to be set on the new platform. Compare them between staging and production while you’re at it, since some may be set differently in each environment.&lt;/p&gt;

&lt;p&gt;We did the same with the dynos: we looked at their types and sizes during discovery, and confirmed them when each service moved, so that it would have roughly the same resources on the new platform as it had on Heroku.&lt;/p&gt;

&lt;p&gt;If you have more than a couple of apps, or you’d rather keep a copy of what you found, the Heroku CLI shows the same information. &lt;code&gt;heroku addons&lt;/code&gt; lists what is attached to an app, &lt;code&gt;heroku config&lt;/code&gt; lists its config vars, &lt;code&gt;heroku ps&lt;/code&gt; shows its process types with the size and number of dynos for each one, and &lt;code&gt;heroku pg:info&lt;/code&gt; shows the Postgres version, plan, and data size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;heroku addons &lt;span class="nt"&gt;-a&lt;/span&gt; your-app-production
heroku config &lt;span class="nt"&gt;-a&lt;/span&gt; your-app-production
heroku ps &lt;span class="nt"&gt;-a&lt;/span&gt; your-app-production
heroku pg:info &lt;span class="nt"&gt;-a&lt;/span&gt; your-app-production

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looping over &lt;code&gt;heroku pg:info&lt;/code&gt; is also a quick way to &lt;a href="https://www.fastruby.io/blog/upgrading-a-deprecated-heroku-postgres-database" rel="noopener noreferrer"&gt;check the Postgres version of every app you have&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Config vars hold secrets, so be careful about where that output ends up. If you only need the names, for a checklist or to compare environments, you can print the keys and diff them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;heroku config &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; your-app-staging | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'keys[]'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;heroku config &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; your-app-production | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'keys[]'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Every component is its own migration
&lt;/h2&gt;

&lt;p&gt;Heroku shows you what runs there, but not how each piece got there. On our project, the services weren’t all deployed the same way. The image proxy, for example, ran straight from an upstream container image, with only configuration on top, while other services had build steps of their own. On Heroku they all look like any other process type, and nothing there tells you which one depends on someone else’s image and which one depends on a build you will have to reproduce.&lt;/p&gt;

&lt;p&gt;That difference decides what the move looks like. For a service that runs from an upstream image, you need to know where the image comes from and whether the new platform can pull it. For a service with its own build, whether that’s a Dockerfile, a buildpack, or a shell script, you need to read it, because it is often the only place where the build is written down.&lt;/p&gt;

&lt;p&gt;Reading the code is part of discovery, so we went through each repo. The YAML config files described the services they deployed, and they answered many of our questions about how each one was built. When something in them didn’t make sense, that became a question for the client’s team.&lt;/p&gt;

&lt;p&gt;Treat each component as a small migration of its own, with its own build step, config, and move order, instead of assuming that what works for the main app will work for everything around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add-ons, workers, and scheduled jobs
&lt;/h2&gt;

&lt;p&gt;For most add-ons, moving means provisioning the equivalent service on the new platform and changing a URL in the config vars. The ones to look for are add-ons with no direct equivalent. Ours was a Memcached add-on, which many platforms don’t offer as a managed service, so we replaced it with a Redis cache.&lt;/p&gt;

&lt;p&gt;During a migration, there is usually a period where both platforms can run the app. Web processes and workers can run on both for a while, as long as they talk to the same Redis, but scheduled jobs should run on exactly one platform at a time. List everything that runs on a schedule, whether it’s Heroku Scheduler, a clock process in the &lt;code&gt;Procfile&lt;/code&gt;, or a gem like sidekiq-cron, and decide where each one runs during the transition. Check, too, which jobs must never run twice at once and whether anything in the code prevents it, since two copies of a job that calls a rate-limited API can use up a quota that one was designed to stay under.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploys, DNS, and alerting
&lt;/h2&gt;

&lt;p&gt;Check how code reaches production. The Heroku pipeline showed a staging app and a production app, but promotion between them was handled outside Heroku, by a third-party automation that promoted a commit from staging after a set period. The client’s team also relied on review apps, so those went on our list of things to rebuild.&lt;/p&gt;

&lt;p&gt;Find out who controls DNS. The client’s domains were at a third-party registrar, pointing at Heroku’s DNS targets, and the client’s team made the change at cutover. Lower the TTL on every record you’ll change, and do it well ahead of the window: at least as far ahead as the record’s current TTL. Resolvers can keep serving the old value for that long, so with a 24-hour TTL, you’d lower it at least a day before.&lt;/p&gt;

&lt;p&gt;Finally, look at alerting and traffic. The client’s alerts went to Slack and PagerDuty, so decide which triggers to pause during each window and who turns them back on. We used the app’s tracing and database metrics to find the hours with the least traffic, then confirmed those traffic cycles with the client’s team.&lt;/p&gt;

&lt;h2&gt;
  
  
  What discovery missed
&lt;/h2&gt;

&lt;p&gt;A week of discovery didn’t find everything. Two things got past us, and for each of them there was a check that would have caught it earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extensions in a different schema in production
&lt;/h3&gt;

&lt;p&gt;The first one was in the database. Between August 2022 and mid-2023, Heroku &lt;a href="https://help.heroku.com/ZOFBHJCJ/heroku-postgres-extension-schema-restrictions" rel="noopener noreferrer"&gt;installed Postgres extensions into its own &lt;code&gt;heroku_ext&lt;/code&gt; schema&lt;/a&gt;, and extensions created during that time are still there unless someone asked Heroku Support to move them. On this app, production’s &lt;code&gt;hstore&lt;/code&gt;, &lt;code&gt;cube&lt;/code&gt;, and &lt;code&gt;earthdistance&lt;/code&gt; extensions lived in &lt;code&gt;heroku_ext&lt;/code&gt;, while staging’s lived in &lt;code&gt;public&lt;/code&gt;, and nobody knew the two environments differed. The staging rehearsal went smoothly because staging had nothing in &lt;code&gt;heroku_ext&lt;/code&gt;, but once we moved the production database, the app failed at runtime with this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permission denied for schema heroku_ext

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What would have caught it is comparing the two databases before moving either one. In &lt;code&gt;psql&lt;/code&gt;, &lt;code&gt;\dx&lt;/code&gt; lists the installed extensions with their versions and schemas, and &lt;code&gt;\dn+&lt;/code&gt; lists the schemas with their access privileges. Running both on staging and on production and comparing the output would have shown the difference in a few minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis connections from Heroku timing out
&lt;/h3&gt;

&lt;p&gt;The second one was Redis. We initially planned to move Redis to the new platform during the database downtime, while the web app was still running on Heroku. That meant the app had to reach the new Redis instances from Heroku over external connections, and it errored while waiting on the &lt;code&gt;AUTH&lt;/code&gt; response, because the default timeout is one second.&lt;/p&gt;

&lt;p&gt;We were moving to Render, and the cause was specific to how &lt;a href="https://render.com/docs/key-value" rel="noopener noreferrer"&gt;Render Key Value&lt;/a&gt;, its Redis-compatible service, handles connections from outside Render. Key Value only accepts internal connections by default. Once you allow external ones, each new connection has to log in with &lt;code&gt;AUTH&lt;/code&gt; before it can do anything else. Normal commands were fast, but &lt;code&gt;AUTH&lt;/code&gt; took between one and four seconds, longer than the client’s one-second timeout, and the app kept just under 400 connections open at any given time. The client treats &lt;code&gt;AUTH&lt;/code&gt; like any other command, so the only fix on the app’s side was a longer timeout for every command, which would also make real Redis problems slower to surface. Moving only part of the data wasn’t an option either: Redis held the Sidekiq job state and the GraphQL subscriptions, and every instance of the app had to use the same Redis.&lt;/p&gt;

&lt;p&gt;So we changed the plan: move the full app first, keep using Heroku’s Redis for a bit after the move, and then take a short downtime to move Redis as well. The check that would have caught this earlier is connecting from the app’s own Redis client, with its own config, over the network path it will actually use during the transition, before you plan a window around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What discovery should leave you with
&lt;/h2&gt;

&lt;p&gt;By the end of discovery, you want a written answer for every component: how it runs today, what it connects to, and what replaces it on the new platform. A table is the simplest way to keep that in one place. For the app we moved, it looks something like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;How it runs today&lt;/th&gt;
&lt;th&gt;Connected to&lt;/th&gt;
&lt;th&gt;Replacement on the new platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;Heroku Postgres&lt;/td&gt;
&lt;td&gt;web, workers, ETL sync&lt;/td&gt;
&lt;td&gt;managed Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web (Rails API)&lt;/td&gt;
&lt;td&gt;Heroku buildpack&lt;/td&gt;
&lt;td&gt;DNS, alerting&lt;/td&gt;
&lt;td&gt;container image built in CI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Workers and cron&lt;/td&gt;
&lt;td&gt;Heroku buildpack&lt;/td&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;same image as web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;two attached add-ons&lt;/td&gt;
&lt;td&gt;web, workers&lt;/td&gt;
&lt;td&gt;managed Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache&lt;/td&gt;
&lt;td&gt;Memcached add-on&lt;/td&gt;
&lt;td&gt;web&lt;/td&gt;
&lt;td&gt;Redis cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image proxy&lt;/td&gt;
&lt;td&gt;upstream container image&lt;/td&gt;
&lt;td&gt;web&lt;/td&gt;
&lt;td&gt;the same upstream image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ETL sync&lt;/td&gt;
&lt;td&gt;owned by the client’s team&lt;/td&gt;
&lt;td&gt;Postgres&lt;/td&gt;
&lt;td&gt;updated by the client’s team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Once we had answers for every component, we were free to propose the order of the migration, and the table pointed to it. We moved both databases first, staging and then production. Next came the supporting services on staging, the telemetry pieces and the image proxy, and then the staging web app and workers together with Redis.&lt;/p&gt;

&lt;p&gt;Production followed a similar pattern: the telemetry pieces and the image proxy first, then the web app and workers, which we brought up on Render independently of what was running on Heroku while DNS still pointed there and the app kept using Heroku’s Redis. The last step was a second cutover, where the client’s team changed DNS and we migrated Redis.&lt;/p&gt;

&lt;p&gt;That order held up. The one time the order itself caused a problem was when we tried to move Redis early, during the database downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we went through what we inventory before moving an app off Heroku. Heroku’s dashboard and CLI only cover part of that inventory. The things that mattered most on our migration lived in other repos, at the DNS registrar, in tools outside Heroku that read from the database, and in the workflow that promoted code to production. Sorting out early who owns each of those, and giving every component a row in the table, is what let us propose the migration order with confidence.&lt;/p&gt;

&lt;p&gt;Not every app needs a week. If yours is a web process and a database, the move can take &lt;a href="https://www.fastruby.io/blog/migrating-from-heroku-to-railway" rel="noopener noreferrer"&gt;about an hour&lt;/a&gt;. Even our week of discovery missed two things. A staging rehearsal catches a lot, but only for what staging and production actually share, so compare the two before you trust the rehearsal.&lt;/p&gt;

&lt;p&gt;Planning a move off Heroku? Take a look at &lt;a href="https://www.fastruby.io/blog/migrate-from-heroku" rel="noopener noreferrer"&gt;how we approach Heroku migrations&lt;/a&gt;, and &lt;a href="https://www.fastruby.io/#contactus" rel="noopener noreferrer"&gt;send us a message&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>devops</category>
    </item>
    <item>
      <title>Making AI Agents Safe to Retry</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:39:27 +0000</pubDate>
      <link>https://dev.to/fastruby/making-ai-agents-safe-to-retry-1e2f</link>
      <guid>https://dev.to/fastruby/making-ai-agents-safe-to-retry-1e2f</guid>
      <description>&lt;p&gt;Over in the OmbuLabs.ai blog, &lt;a href="https://www.ombulabs.ai/blog/react-agent" rel="noopener noreferrer"&gt;our post on the ReAct pattern in Ruby&lt;/a&gt; walked through building an agent that reasons about a task, calls tools, and acts on the results, all inside a single Ruby script. That’s fine for a command-line experiment. It’s a different story once that same agent needs to run inside a Rails app, triggered by a web request, where a multi-step reasoning loop can easily take longer than any request should stay open.&lt;/p&gt;

&lt;p&gt;The obvious fix is to move the agent into a background job. Sidekiq already handles work that takes a while, so an agent’s loop of thinking, searching, and acting should fit right in. It does, mostly, but a job’s retry semantics were built around retrying a database write, not a chain of API calls that has real, sometimes irreversible, side effects.&lt;/p&gt;

&lt;p&gt;In this article, you’ll learn why an agent’s reasoning loop doesn’t survive a naive move into a background job, how to make its side effects idempotent against Sidekiq retries, and how to stream its progress to the user while it runs.&lt;/p&gt;

&lt;h2 id="why-agents-dont-belong-in-the-request-cycle"&gt;Why Agents Don’t Belong in the Request Cycle&lt;/h2&gt;

&lt;p&gt;Picture a support ticket coming into a Rails app. A reasonable first instinct is to trigger an agent right there in the controller: it reads the ticket, searches internal docs for anything relevant, drafts a reply, and tags the ticket before the response ever renders. Each of those is a step in the ReAct loop, thought, action, observation, repeated until the agent decides it’s done.&lt;/p&gt;

&lt;p&gt;The trouble is that each step is also a network call to an LLM provider, plus whatever tool it decides to use. One search, one draft, one tag isn’t so bad. A ticket that needs two rounds of searching before the agent has enough context to draft a reply is a different story, and there’s no way to know in advance how many rounds a given ticket will need. On Heroku, &lt;a href="https://devcenter.heroku.com/articles/request-timeout" rel="noopener noreferrer"&gt;a web request has 30 seconds&lt;/a&gt; before the router gives up and returns an H12 timeout, and that’s an optimistic budget for a loop whose length depends on what the model decides to do next.&lt;/p&gt;

&lt;p&gt;Holding a web worker open for that long is also just wasteful. Puma has a fixed number of worker threads, and every request stuck waiting on an agent is a thread that isn’t serving anyone else. This is the same argument that’s applied to slow jobs since long before agents existed: &lt;a href="https://www.ombulabs.ai/blog/sidekiq/processing-a-csv-file-in-batch-with-sidekiq.html" rel="noopener noreferrer"&gt;a 10,000+ row CSV upload was enough to time out a browser&lt;/a&gt;. It’s just that an agent’s loop is often slower and less predictable than a fixed-size file.&lt;/p&gt;

&lt;p&gt;Sidekiq is the obvious place to put this kind of work: queue a job, return a response immediately, let the ticket update once the job finishes. The rest of this article is about what happens once you actually do that.&lt;/p&gt;

&lt;h2 id="moving-the-agent-into-a-sidekiq-job"&gt;Moving the Agent into a Sidekiq Job&lt;/h2&gt;

&lt;p&gt;We’ll build on the same shape as the ReAct pattern post: an agent with a few tools, looping through thought, action, and observation until it’s done. Here, the tools are specific to ticket triage, searching internal docs, drafting a reply, and tagging the ticket:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/services/ticket_triage/tools.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;TicketTriage&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tools&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;InternalDocs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:summary&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;InternalDocs&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
      &lt;span class="s2"&gt;"Search failed: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;draft_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;draft_reply: &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"drafted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="s2"&gt;"Draft saved."&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tag_ticket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;tags: &lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="s2"&gt;"Tagged with &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The function definitions follow the same JSON shape Claude expects, one per tool:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="no"&gt;FUNCTION_DEFINITIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"search_docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Search internal documentation for relevant context"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;input_schema: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;properties: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;query: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"What to search for"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"draft_reply"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Save a draft reply to the ticket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;input_schema: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;properties: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"The drafted reply text"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"tag_ticket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"Apply tags to the ticket for routing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;input_schema: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;properties: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;tags: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;items: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;required: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The agent itself is close to the &lt;code class="language-plaintext highlighter-rouge"&gt;ReActAgent&lt;/code&gt; class from the earlier post, adapted to work on a single ticket instead of a blog topic:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/services/ticket_triage/agent.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;TicketTriage&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;
      &lt;span class="vi"&gt;@tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
      &lt;span class="vi"&gt;@messages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="s2"&gt;"Triage this ticket:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="vi"&gt;@messages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:tool_use&lt;/span&gt;

          &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:input&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
          &lt;span class="vi"&gt;@messages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"tool_result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;tool_use_id: &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"search_docs"&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="vi"&gt;@tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"draft_reply"&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="vi"&gt;@tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draft_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:content&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"tag_ticket"&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="vi"&gt;@tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tag_ticket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:tags&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s2"&gt;"Unknown tool"&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;
      &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;max_tokens: &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;system: &lt;/span&gt;&lt;span class="no"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;messages: &lt;/span&gt;&lt;span class="vi"&gt;@messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;tools: &lt;/span&gt;&lt;span class="no"&gt;FUNCTION_DEFINITIONS&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;client&lt;/span&gt;
      &lt;span class="vi"&gt;@client&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;Anthropic&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;api_key: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ANTHROPIC_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Wrapping this in a job is, at first, almost anticlimactic:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/jobs/ticket_triage_job.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TicketTriageJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;TicketTriage&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And the call site, wherever a ticket gets created:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
  &lt;span class="vi"&gt;@ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;TicketTriageJob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="vi"&gt;@ticket&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The controller returns right away, the agent does its work on a Sidekiq worker, and the ticket’s &lt;code class="language-plaintext highlighter-rouge"&gt;draft_reply&lt;/code&gt; and &lt;code class="language-plaintext highlighter-rouge"&gt;tags&lt;/code&gt; show up whenever the job finishes. This is the version that works when everything goes right. The next section is about what happens when it doesn’t.&lt;/p&gt;

&lt;h2 id="retries-replay-the-whole-loop"&gt;Retries Replay the Whole Loop&lt;/h2&gt;

&lt;p&gt;Sidekiq’s default behavior on an unhandled exception is to re-enqueue the job and try again later, with a backoff, up to 25 times unless you configure otherwise. This works well for the kind of job Sidekiq was originally built around: something like resizing an uploaded image or writing a row, where the whole method is safe to run again from the top, or already guards itself with a &lt;code class="language-plaintext highlighter-rouge"&gt;find_or_create_by&lt;/code&gt; or a unique index.&lt;/p&gt;

&lt;p&gt;&lt;code class="language-plaintext highlighter-rouge"&gt;TicketTriageJob#perform&lt;/code&gt; doesn’t have that property, because Sidekiq has no idea what happened inside the agent’s loop. It only knows whether &lt;code class="language-plaintext highlighter-rouge"&gt;perform&lt;/code&gt; raised or returned. If the database connection drops while tagging the ticket, after the agent has already searched the docs and drafted a reply, the job as a whole raises, and Sidekiq schedules a full retry of &lt;code class="language-plaintext highlighter-rouge"&gt;perform&lt;/code&gt;, not a retry of “just that last step.”&lt;/p&gt;

&lt;p&gt;Here’s roughly what that looks like across the two attempts:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;Attempt 1:
💬 Thought: Let me search for relevant documentation first.
🔧 Action: search_docs({query: "refund policy digital goods"})
📝 Observation: Refunds are issued within 14 days...

💬 Thought: I have enough context to draft a reply.
🔧 Action: draft_reply({content: "Thanks for reaching out! Per our..."})
📝 Observation: Draft saved.

💬 Thought: This looks like a billing issue, let me tag it.
🔧 Action: tag_ticket({tags: ["billing", "refund"]})
📝 Observation: ActiveRecord::ConnectionTimeoutError: could not obtain a database connection within 5.000 seconds
[job raises, Sidekiq schedules a retry]

Attempt 2:
💬 Thought: Let me search for relevant documentation first.
🔧 Action: search_docs({query: "refund policy digital goods"})
📝 Observation: Refunds are issued within 14 days...

💬 Thought: I have enough context to draft a reply.
🔧 Action: draft_reply({content: "Hi there, thank you for contacting us..."})
📝 Observation: Draft saved.
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Two things went wrong here that would be easy to miss in a quick look at the logs. The search happened twice, which is a wasted API call and a few cents, not a disaster on its own. The draft happened twice, and because the model isn’t deterministic between calls, the second draft isn’t the same text as the first, it just quietly replaced it. If &lt;code class="language-plaintext highlighter-rouge"&gt;draft_reply&lt;/code&gt; had gone one step further and actually sent the reply to the customer instead of just saving it, attempt two would have sent a second, different email for the same ticket.&lt;/p&gt;

&lt;p&gt;None of this is a Sidekiq bug. Sidekiq is doing exactly what it promises: retrying the job. The mismatch is that “the job” here is a loop with several external, stateful side effects glued together, and retrying it as one unit assumes every step inside is either free to repeat or already protects itself. Neither is true yet in this version.&lt;/p&gt;

&lt;h2 id="making-the-agents-side-effects-idempotent"&gt;Making the Agent’s Side Effects Idempotent&lt;/h2&gt;

&lt;p&gt;The fix isn’t to stop Sidekiq from retrying, it’s to make each side-effecting tool check whether it already did its job before doing it again. &lt;code class="language-plaintext highlighter-rouge"&gt;search_docs&lt;/code&gt; doesn’t need this, it only reads, so repeating it wastes a little time and a little API budget but changes nothing. &lt;code class="language-plaintext highlighter-rouge"&gt;draft_reply&lt;/code&gt; and &lt;code class="language-plaintext highlighter-rouge"&gt;tag_ticket&lt;/code&gt; do need it, because each one writes something that a second, different write would silently clobber or duplicate:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/services/ticket_triage/tools.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;TicketTriage&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Tools&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ticket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="vi"&gt;@ticket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ticket&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_docs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;InternalDocs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:summary&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;InternalDocs&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
      &lt;span class="s2"&gt;"Search failed: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;draft_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Already drafted, skipping."&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draft_reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;

      &lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;draft_reply: &lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s2"&gt;"drafted"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="s2"&gt;"Draft saved."&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tag_ticket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;"Already tagged, skipping."&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;

      &lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;tags: &lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="s2"&gt;"Tagged with &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The guard clause in each method reads the ticket’s own state as the record of what already happened, rather than adding a separate idempotency-key table. This works here because the ticket’s fields double as their own completion markers: a &lt;code class="language-plaintext highlighter-rouge"&gt;draft_reply&lt;/code&gt; isn’t just an input to the next step, it’s proof the step already ran. A job whose side effects don’t leave that kind of trace on an existing record, sending a Slack message with no corresponding column to check, say, would need something more explicit, like a &lt;code class="language-plaintext highlighter-rouge"&gt;processed_steps&lt;/code&gt; array on the ticket, or a Redis key set right before the external call.&lt;/p&gt;

&lt;p&gt;The short string each guard returns matters as much as the guard itself. It goes back into the agent’s message history as a normal tool result, so the model reads “Already drafted, skipping.” the same way it would read “Draft saved.”, and moves on to the next step instead of getting stuck or trying the same action again. Replaying attempt two from the earlier trace with these guards in place, the search still runs twice (harmless), the draft short-circuits on the second attempt (no second, different email going out), and the tag, which is what actually failed the first time, gets a real chance to succeed.&lt;/p&gt;

&lt;h2 id="streaming-progress-to-the-user"&gt;Streaming Progress to the User&lt;/h2&gt;

&lt;p&gt;The agent is now safe to retry, but from the ticket page it’s invisible. Someone looking at the ticket while the job runs sees nothing change for however long the loop takes, then the draft reply and tags appear all at once. That’s a worse experience than the synchronous version was, even though it’s more robust underneath.&lt;/p&gt;

&lt;p&gt;Turbo Streams can close that gap without adding a polling endpoint or a websocket layer of our own. The usual Hotwire pattern broadcasts from an ActiveRecord callback, and &lt;a href="https://www.ombulabs.ai/blog/rails/hotwire/hotwire-demo.html" rel="noopener noreferrer"&gt;broadcasting directly from a job used to be the trickier path&lt;/a&gt; when we first tried it. What’s different here is that &lt;code class="language-plaintext highlighter-rouge"&gt;TicketTriageJob&lt;/code&gt; runs once and finishes, it isn’t a self-rescheduling poller broadcasting on every tick indefinitely, so calling &lt;code class="language-plaintext highlighter-rouge"&gt;Turbo::StreamsChannel.broadcast_append_to&lt;/code&gt; directly from the agent, the way we do below, stays simple for this bounded sequence of steps. We broadcast a small update at each step of the loop:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/services/ticket_triage/agent.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;TicketTriage&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;
      &lt;span class="n"&gt;broadcast_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Starting triage..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="vi"&gt;@messages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="s2"&gt;"Triage this ticket:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;complete&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="vi"&gt;@messages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="ss"&gt;:tool_use&lt;/span&gt;

          &lt;span class="n"&gt;broadcast_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Calling &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:input&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
          &lt;span class="n"&gt;broadcast_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

          &lt;span class="vi"&gt;@messages&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;type: &lt;/span&gt;&lt;span class="s2"&gt;"tool_result"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;tool_use_id: &lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;broadcast_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Triage complete."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;private&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;broadcast_step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="no"&gt;Turbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StreamsChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_append_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="vi"&gt;@ticket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"agent_activity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"tickets/agent_step"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;text: &lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# dispatch, complete, and client stay as they were&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;The partial being appended is about as plain as a Turbo Stream target gets:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;%# app/views/tickets/_agent_step.html.erb %&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"agent-step"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;And the ticket’s show page just needs to subscribe to the stream and give the broadcasts somewhere to land:&lt;/p&gt;

&lt;pre class="highlight"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream_from&lt;/span&gt; &lt;span class="vi"&gt;@ticket&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"agent_activity"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- agent steps get appended here as the job runs --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Whoever has the ticket open watches the activity fill in step by step instead of staring at an unchanged page. One thing worth deciding deliberately rather than by default: &lt;code class="language-plaintext highlighter-rouge"&gt;result.to_s&lt;/code&gt; broadcasts whatever the tool returned, including the full text of a drafted reply. That’s fine for an internal support queue, but if this pattern gets reused somewhere a customer might see the stream, broadcast a short status (“Draft ready for review”) instead of the tool’s raw output.&lt;/p&gt;

&lt;p&gt;That covers live progress for whoever has the ticket open, which is a different need from knowing what each step cost after the fact. If you also want to track token usage, latency, and dollar cost per call, see &lt;a href="https://www.fastruby.io/blog/instrumenting-llm-calls-in-rails-with-event-notify" rel="noopener noreferrer"&gt;Tracking LLM Latency &amp;amp; Cost with Rails Events&lt;/a&gt;, which wires that up with the Rails 8.1 Event Reporter.&lt;/p&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Getting an agent out of the request cycle and into Sidekiq was the easy part, and it’s also the part that looks finished before it actually is. The &lt;code class="language-plaintext highlighter-rouge"&gt;TicketTriageJob&lt;/code&gt; from the second section runs, and runs correctly, right up until something fails partway through and Sidekiq does exactly what it’s supposed to do: retry the whole thing. Making that safe meant treating the ticket’s own fields as a record of which steps already happened, so a retried loop skips what’s done instead of repeating it. Making it visible meant broadcasting each step over Turbo Streams as it happens, since a job that’s safe to retry silently is still a bad experience if it looks frozen for a minute at a time.&lt;/p&gt;

&lt;p&gt;None of this is free complexity. A tool that only reads, or an agent that makes a single classification call and returns, doesn’t need idempotency guards because there’s nothing stateful to duplicate. These patterns earn their keep specifically when an agent’s loop can run more than once, touches something outside the database, and takes long enough that a user is watching it happen, which describes more production agent work than not, but not all of it. It’s also worth confirming an agent is the right feature for the job in the first place, before any of this gets built, and we’ve covered &lt;a href="https://www.fastruby.io/blog/from-ai-opportunity-to-ai-feature" rel="noopener noreferrer"&gt;how to prioritize which AI features to build in a Rails app&lt;/a&gt; elsewhere. Streaming progress to the user, as covered here, is also a different problem from tracing what an agent did for your own debugging after the fact, &lt;a href="https://www.ombulabs.ai/blog/why-llm-applications-need-tracing" rel="noopener noreferrer"&gt;we’ve written separately about why LLM applications need that kind of observability&lt;/a&gt; too.&lt;/p&gt;

&lt;p&gt;Running agents in production and hitting failure modes like these? &lt;a href="/#contact-us"&gt;Let’s talk about it&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Irish Chess Union Upgrades to Rails 8.1</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Thu, 17 Sep 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/etagwerker/irish-chess-union-upgrades-to-rails-81-1ml1</link>
      <guid>https://dev.to/etagwerker/irish-chess-union-upgrades-to-rails-81-1ml1</guid>
      <description>&lt;p&gt;Founded in 1912, the Irish Chess Union (ICU) is the governing body for chess across the island of Ireland. It has about 2,500 members and run entirely by volunteers.&lt;/p&gt;

&lt;p&gt;Its website, &lt;a href="https://www.icu.ie/" rel="noopener noreferrer"&gt;icu.ie&lt;/a&gt;, has been maintained by webmaster Jonathan O’Connor since 2015. Earlier this year, Jonathan reached out to us. He wanted help taking the ICU’s Rails application from 7.0 to 8.1.&lt;/p&gt;

&lt;p&gt;We used Claude Code and our open source &lt;a href="https://github.com/ombulabs/claude-code_rails-upgrade-skill" rel="noopener noreferrer"&gt;Claude Code Rails Upgrade Skill&lt;/a&gt; to get it done. We also recorded the sessions as a video series, so other teams could see the methodology in practice.&lt;/p&gt;

&lt;p&gt;In this article, we’ll walk through the upgrade, one version at a time, and share what Jonathan thought of the process.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa26ogyyq80ti8kfwooiv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa26ogyyq80ti8kfwooiv.png" alt="The Irish Chess Union homepage at icu.ie" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Here was our challenge
&lt;/h2&gt;

&lt;p&gt;The ICU’s application was built and is maintained by professionals, just not on the clock. Everyone involved works on it nights and weekends, as volunteers. It was written mostly by Mark Orr, a ratings officer who had just retired and wanted to give back to the community.&lt;/p&gt;

&lt;p&gt;That project became learning Ruby on Rails and building the ICU a website from scratch. He wrote about 90% of the code that’s still running today. Jonathan took over as webmaster in 2015.&lt;/p&gt;

&lt;p&gt;He’d learned Ruby a few years earlier, on a six-week trip to Rwanda building credit card payment systems. Rails’ conventions weren’t as disorienting to him as they are to developers who come to it cold.&lt;/p&gt;

&lt;p&gt;He kept the application current on his own for a while. When he upgraded it from Rails 3 to 5, he did it the patient way: bumping the Gemfile one patch version at a time, running the test suite, and fixing whatever the deprecation warnings in the logs pointed at.&lt;/p&gt;

&lt;p&gt;His colleague Ben Murray later carried it from 5.0 to 7.0. Then Jonathan tried to take it from 7.0 to 7.1 by himself. The same approach that had worked for years stopped working.&lt;/p&gt;

&lt;p&gt;He’d been away from daily Rails work since 2016. The deprecation warnings piled up faster than he could make sense of them.&lt;/p&gt;

&lt;p&gt;“It was a lot harder,” he said. Eventually it became impossible to tell which warnings actually mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here’s how we solved it
&lt;/h2&gt;

&lt;p&gt;We used the same methodology we’ve documented for years: &lt;a href="///blog/upgrade-rails/dual-boot/dual-boot-with-rails-6-0-beta.html"&gt;dual-boot the app&lt;/a&gt; with the &lt;a href="https://github.com/fastruby/next_rails" rel="noopener noreferrer"&gt;&lt;code&gt;next_rails&lt;/code&gt;&lt;/a&gt; gem, and upgrade one minor Rails version at a time, never skipping a hop.&lt;/p&gt;

&lt;p&gt;For the ICU, that meant an unhurried climb from 7.0 to 7.1, then 7.2, then 8.0, and finally 8.1. Each hop got its own pull request. Each one had a green test suite against both Rails versions before we merged it and moved to the next.&lt;/p&gt;

&lt;p&gt;Here’s what that looks like under the hood, for anyone who hasn’t dual-booted a Ruby app before:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faj8a6x48ezm627sbfxz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faj8a6x48ezm627sbfxz6.png" alt="How dual booting works: two Gemfiles, two Rails versions, verified independently" width="799" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before we touched the Rails version at all, we spent some time on groundwork.&lt;/p&gt;

&lt;p&gt;We set up GitHub Actions, so the ICU had CI for the first time. We patched the Docker configuration, so the app could build against either Gemfile.&lt;/p&gt;

&lt;p&gt;That kind of scaffolding is easy to underestimate. It’s usually what makes the rest of the upgrade boring, in a good way.&lt;/p&gt;

&lt;p&gt;This was also a great opportunity to share with the community how we leverage our open source Claude Code Rails Upgrade Skill, instead of just publishing it and hoping people would try it themselves. In 3 different pairing sessions, we drove Claude to use the skill to handle the mechanical work that happens in every upgrade project.&lt;/p&gt;

&lt;p&gt;Claude handled a lot of the mechanical work in each hop: previewing what &lt;code&gt;rails app:update&lt;/code&gt; would change, running the test suite against both Gemfiles, and flagging deprecation warnings before they piled up.&lt;/p&gt;

&lt;p&gt;Our engineers, &lt;a href="https://dev.to/blog/authors/hmdros"&gt;Henrique Medeiros&lt;/a&gt;, &lt;a href="https://dev.to/blog/authors/arieljuod"&gt;Ariel Juodziukynas&lt;/a&gt;, &lt;a href="https://dev.to/blog/authors/aisayo"&gt;Aysan Isayo&lt;/a&gt;, and &lt;a href="https://dev.to/blog/authors/juanvqz"&gt;Juan Vasquez&lt;/a&gt;, made the judgment calls the skill is deliberately not built to make on its own.&lt;/p&gt;

&lt;p&gt;One of those calls was a small shim in the ICU’s codebase. Even Jonathan, after nearly a decade of maintaining the app, said he never would have found it on his own.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“There’s one thing I would never have found, even if I kept working at trying to figure out how to do the upgrade myself, and that was this shim in our code,” Jonathan said. “I think that’s one of the places where you earn your keep, because these are tricks that people won’t know, since they’re not doing upgrades every day.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Part of why the rest of the upgrade went smoothly is that the ICU’s codebase is, in Jonathan’s own words, unusually well-behaved.&lt;/p&gt;

&lt;p&gt;No monkeypatches. No reopened core classes. No weird code in weird places.&lt;/p&gt;

&lt;p&gt;And on top of that: A real test suite with both model and integration coverage.&lt;/p&gt;

&lt;p&gt;None of that is guaranteed for an application built by a single volunteer. But Mark Orr and Jonathan had both stuck close to the Rails way, and it showed in how predictable each hop turned out to be.&lt;/p&gt;

&lt;p&gt;The ICU never had to pause its own work, either.&lt;/p&gt;

&lt;p&gt;Jonathan’s small team of volunteer contributors kept shipping features throughout the upgrade. The app’s Stripe integration, the dependency that worried us most going in since it handles ICU’s membership payments, never broke stride.&lt;/p&gt;

&lt;h2&gt;
  
  
  See the results
&lt;/h2&gt;

&lt;p&gt;Now the ICU is current on Rails 8.1.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I didn’t notice any issues at all. There were no breakages at all. You made maybe seven or eight pull requests, probably thirty or forty commits over that time, and they just fitted seamlessly into all our changes. Nothing ever broke. And thank you so much for setting up our Docker, because now we have our tests running on GitHub, which is brilliant.”&lt;/p&gt;

&lt;p&gt;Jonathan O’Connor, Webmaster, Irish Chess Union&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here’s one of those pull requests, upgrading the app to Rails 7.2:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkjo4dh1z8qald1xsex9m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkjo4dh1z8qald1xsex9m.png" alt="Pull request #157, upgrading the ICU application to Rails 7.2" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Eight pull requests, somewhere around thirty to forty commits, and &lt;strong&gt;zero breakages&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That’s not a coincidence. It’s what a sequential, dual-booted upgrade against a clean codebase and a stable test suite looks like when it goes right.&lt;/p&gt;

&lt;p&gt;There’s also a quieter story here, about who can afford this kind of work.&lt;/p&gt;

&lt;p&gt;A volunteer-run federation with no engineering budget to speak of got a five-version Rails upgrade done. Not because the work got easier, but because one senior engineer driving Claude Code and our upgrade skill can now get much closer to what it used to take two.&lt;/p&gt;

&lt;p&gt;This is a real shift we’re seeing across our upgrade engagements, and it’s a big part of why an organization like the ICU can now afford this kind of work at all.&lt;/p&gt;

&lt;p&gt;If you’re trying to make this case internally, &lt;a href="///blog/rails/upgrade/how-to-pitch-an-upgrade-to-your-boss.html"&gt;here’s some advice on how to pitch a Rails upgrade to your boss&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch the pairing sessions
&lt;/h2&gt;

&lt;p&gt;We recorded four sessions from this project, so you can watch the actual pairing work instead of just reading about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 1: Setting up CI
&lt;/h3&gt;

&lt;p&gt;Henrique Medeiros and I spend about an hour setting up CI for the ICU’s application: wiring up GitHub Actions and using Claude Code to work through the setup issues that came up along the way:&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 2: Dual booting and API compatibility
&lt;/h3&gt;

&lt;p&gt;Henrique and I dig into dual booting and the API differences between Rails 7.0 and 7.1.&lt;/p&gt;

&lt;p&gt;We set up &lt;code&gt;next_rails&lt;/code&gt; and a &lt;code&gt;Gemfile&lt;/code&gt; symlink strategy, chase down a few gem compatibility issues using &lt;a href="https://www.railsbump.org/" rel="noopener noreferrer"&gt;RailsBump.org&lt;/a&gt;, and configure GitHub Actions and Docker to run the test suite against both Rails versions:&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 3: Upgrading to Rails 7.2, and getting ready for 8.0
&lt;/h3&gt;

&lt;p&gt;Juan Vasquez and I carry the app from Rails 7.1 to 7.2, while preparing the codebase for Rails 8.0.&lt;/p&gt;

&lt;p&gt;We walk through how the upgrade skill checks the current patch version, confirms dual boot is set up, and generates an upgrade report, then track down a &lt;a href="https://github.com/irishchessunion/icu_www_app/pull/157" rel="noopener noreferrer"&gt;Capybara and Selenium WebDriver compatibility issue&lt;/a&gt; that turns out to have nothing to do with Rails itself:&lt;/p&gt;

&lt;h3&gt;
  
  
  Recap: A quick recap of the entire upgrade project
&lt;/h3&gt;

&lt;p&gt;Jonathan and I talk about the project end to end: the ICU’s volunteer culture, why the upgrade stalled at Rails 7.1, the results, and Jonathan’s own take on where AI helps and where he still won’t let it near ICU’s data.&lt;/p&gt;

&lt;p&gt;Keep an eye out for the Easter egg: a shim in the ICU’s codebase that Jonathan says he’d never have thought of himself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The ICU’s upgrade worked because the methodology did the heavy lifting, not the AI on its own. Our AI tooling certainly help speed things up! 🚀&lt;/p&gt;

&lt;p&gt;Dual booting, sequential hops, a deprecation-first approach: none of that is new.&lt;/p&gt;

&lt;p&gt;What’s new is that Claude Code, guided by a skill that encodes those opinions, can now execute most of it, while our engineers focus on the handful of judgment calls, like that one shim, that still need a human who’s done this before.&lt;/p&gt;

&lt;p&gt;It’s worth saying plainly: this went smoothly in part because the ICU’s codebase was clean and well-tested.&lt;/p&gt;

&lt;p&gt;A messier application, full of monkeypatches and flaky tests, would have taken longer.&lt;/p&gt;

&lt;p&gt;Has your team been putting off a Rails upgrade? &lt;a href="https://www.fastruby.io/contact" rel="noopener noreferrer"&gt;Let’s talk&lt;/a&gt; and get your up to the latest stable versions of Ruby and Rails!&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;All of the pull requests below were merged into the ICU’s &lt;code&gt;master&lt;/code&gt; branch, and the full history is public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/irishchessunion/icu_www_app/pull/117" rel="noopener noreferrer"&gt;#117: Remove deprecation warnings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/irishchessunion/icu_www_app/pull/134" rel="noopener noreferrer"&gt;#134: Set up GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/irishchessunion/icu_www_app/pull/145" rel="noopener noreferrer"&gt;#145: Docker setup configuration patches&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/irishchessunion/icu_www_app/pull/150" rel="noopener noreferrer"&gt;#150: Dual boot the application (Rails 7.0 and 7.1)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/irishchessunion/icu_www_app/pull/152" rel="noopener noreferrer"&gt;#152: Upgrade to Rails 7.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/irishchessunion/icu_www_app/pull/157" rel="noopener noreferrer"&gt;#157: Upgrade to Rails 7.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/irishchessunion/icu_www_app/pull/165" rel="noopener noreferrer"&gt;#165: Upgrade to Rails 8.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/irishchessunion/icu_www_app/pull/184" rel="noopener noreferrer"&gt;#184: Upgrade to Rails 8.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find the ICU’s full application source code at &lt;a href="https://github.com/irishchessunion/icu_www_app" rel="noopener noreferrer"&gt;github.com/irishchessunion/icu_www_app&lt;/a&gt;, and our own upgrade skill at &lt;a href="https://github.com/ombulabs/claude-code_rails-upgrade-skill" rel="noopener noreferrer"&gt;github.com/ombulabs/claude-code_rails-upgrade-skill&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want the version-by-version details behind each hop, our &lt;a href="///blog/rails/upgrade/rails-upgrade-series.html"&gt;Rails Upgrade Series&lt;/a&gt; covers every jump from 2.3 onward.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>opensource</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Fortify Rails - Defending Your Ruby on Rails Applications from Bad Actors</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Mon, 26 Jun 2023 12:34:56 +0000</pubDate>
      <link>https://dev.to/fastruby/fortify-rails-defending-your-ruby-on-rails-applications-from-bad-actors-25j2</link>
      <guid>https://dev.to/fastruby/fortify-rails-defending-your-ruby-on-rails-applications-from-bad-actors-25j2</guid>
      <description>&lt;p&gt;On Monday June 12th, 2023, FastRuby.io partnered with &lt;a href="https://expeditedsecurity.com/" rel="noopener noreferrer"&gt;Expedited Security&lt;/a&gt; to talk about how to secure your Ruby on Rails application.&lt;/p&gt;

&lt;p&gt;In this free webinar &lt;a href="https://mastodon.social/@etagwerker" rel="noopener noreferrer"&gt;Ernesto Tagwerker&lt;/a&gt; (FastRuby.io) and &lt;a href="https://ruby.social/@mbuckbee" rel="noopener noreferrer"&gt;Mike Buckbee&lt;/a&gt; (Expedited Security) discussed topics of interest related to &lt;a href="https://fastruby.io/blog/tags/security" rel="noopener noreferrer"&gt;Rails security&lt;/a&gt; (exploitable ActiveRecord code, vulnerable dependencies, botnets, DDoS, a breakdown of common threats, and more).&lt;/p&gt;

&lt;p&gt;You’ll also get a sneak peak of &lt;a href="https://wafris.org/" rel="noopener noreferrer"&gt;Wafris&lt;/a&gt;, an Open Source service to prevent attackers and dark traffic to your application and of our new &lt;a href="https://www.fastruby.io/security-audit?utm_source=blog&amp;amp;utm_medium=organic&amp;amp;utm_campaign=webinar" rel="noopener noreferrer"&gt;Rails Security Audit&lt;/a&gt;, a service to detect vulnerabilities and exploitable code in your app!&lt;/p&gt;

&lt;p&gt;Check out the recording of the Fortify Rails webinar here: &lt;a href="https://www.youtube.com/embed/KkXTw7UDR9w" rel="noopener noreferrer"&gt;https://www.youtube.com/embed/KkXTw7UDR9w&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the slides on exploitable code and insecure dependencies: &lt;a href="https://speakerdeck.com/etagwerker/fortify-rails-webinar" rel="noopener noreferrer"&gt;https://speakerdeck.com/etagwerker/fortify-rails-webinar&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to read more about Ruby and Rails security tooling check out this article: &lt;a href="https://www.fastruby.io/blog/rails/security/ruby-security-toolkit.html" rel="noopener noreferrer"&gt;4 Essential Security Tools To Level Up Your Rails Security&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Need a trusted third party to give you a prioritized list of security issues? Check out our &lt;a href="https://www.fastruby.io/security-audit?utm_source=blog&amp;amp;utm_medium=organic&amp;amp;utm_campaign=webinar" rel="noopener noreferrer"&gt;Rails Security Audit&lt;/a&gt; service. &lt;/p&gt;

</description>
      <category>rails</category>
      <category>security</category>
    </item>
    <item>
      <title>Power Home Remodeling Increases Server Speed by 40% with FastRuby.io’s Tune Report</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Fri, 02 Jun 2023 10:10:59 +0000</pubDate>
      <link>https://dev.to/fastruby/power-home-remodeling-increases-server-speed-by-40-with-fastrubyios-tune-report-3e02</link>
      <guid>https://dev.to/fastruby/power-home-remodeling-increases-server-speed-by-40-with-fastrubyios-tune-report-3e02</guid>
      <description>&lt;p&gt;&lt;a href="https://powerhrg.com/" rel="noopener noreferrer"&gt;Power Home Remodeling (Power)&lt;/a&gt; is the nation’s largest full-service exterior home remodeler and a top workplace.&lt;/p&gt;

&lt;p&gt;Headquartered in Chester, Pennsylvania with offices in 18 territories across the United States, the award-winning company’s primary product line includes windows, siding, doors, roofing, solar roofing panels, and attic insulation.&lt;/p&gt;

&lt;p&gt;In this article we will share how our &lt;a href="https://fastruby.io/tune" rel="noopener noreferrer"&gt;Tune Report&lt;/a&gt;  helped Power speed up their application by reducing their average page load time from 5 to 3 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Challenge
&lt;/h2&gt;

&lt;p&gt;Power provides homeowners with top-rated customer service, and quality, energy-efficient products that help upgrade their home’s exterior. While a home remodeling company at its core, Power has also become a leader in workplace culture, leadership development, and technology.&lt;/p&gt;

&lt;p&gt;Behind the scenes, Power’s unique, state-of-the-art collaborative intelligence platform—known as Nitro—enables seamless, real-time communication across the business.&lt;/p&gt;

&lt;p&gt;The company’s technologists are responsible for developing and delivering Nitro’s software through continuous integration, as well as the three data centers that manage it.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://www.linkedin.com/in/wadewinningham" rel="noopener noreferrer"&gt;Wade Winningham&lt;/a&gt;, Principal Developer, “We don’t use third-party applications for our sales, lead generation, tracking, and fulfillment. We’ve built everything ourselves and we’re proud that we operate our own platforms and technology without much reliance on an outside cloud.”&lt;/p&gt;

&lt;p&gt;Wade, who helps set standards for the entire department, is always on the alert for ways to enhance and refresh their technology infrastructure. For the past six years, they have reached out to us anytime they needed support with Ruby on Rails maintenance, development, and &lt;a href="https://fastruby.io/blog/tags/upgrades" rel="noopener noreferrer"&gt;upgrades&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Several years ago, the Ruby on Rails version underlying Power’s software was getting stale, but due to the size of their application, it was going to be a daunting task to handle internally.&lt;/p&gt;

&lt;p&gt;We were hired to help with the &lt;a href="https://fastruby.io/blog/rails/upgrades/upgrade-rails-from-3-2-to-4-0.html" rel="noopener noreferrer"&gt;upgrade from Rails 3.2 to 4.0&lt;/a&gt;. “&lt;a href="https://www.ombulabs.com" rel="noopener noreferrer"&gt;OmbuLabs&lt;/a&gt;/FastRuby.io is an expert in Ruby and Rails, and we knew it was going to be in our best interest to hire them,” explained Wade.&lt;/p&gt;

&lt;p&gt;“&lt;a href="https://www.fastruby.io/blog/rails/upgrades/case-study/upgrading-a-monolith.html" rel="noopener noreferrer"&gt;The Rails upgrade project was a huge success&lt;/a&gt;. The developers were top notch, knew what they were doing, understood our internal application, and helped us through it.” We collaborated with Power’s team to get them all the way to Rails 6.0 and then they continued their upgrade journey.&lt;/p&gt;

&lt;p&gt;Our next challenge was to find ways for our clients to identify optimization opportunities within their Rails applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Tune Report
&lt;/h2&gt;

&lt;p&gt;Recently, in partnership with &lt;a href="https://www.nateberkopec.com/" rel="noopener noreferrer"&gt;Nate Berkopec&lt;/a&gt; from &lt;a href="https://www.speedshop.co/" rel="noopener noreferrer"&gt;Speedshop&lt;/a&gt;, we launched our new &lt;a href="https://fastruby.io/tune" rel="noopener noreferrer"&gt;Tune Report&lt;/a&gt;, which helps our clients accelerate their Ruby applications, reach &lt;a href="https://fastruby.io/blog/tags/performance" rel="noopener noreferrer"&gt;a higher level of performance&lt;/a&gt;, and reduce server costs.&lt;/p&gt;

&lt;p&gt;We shared our new service with Wade and his team, and they immediately saw the benefits. “We thought it would be great to have an outside perspective of our application. The Tune Report would look at areas that we weren’t focusing on or even aware of having potential issues.”&lt;/p&gt;

&lt;h3&gt;
  
  
  See the results:
&lt;/h3&gt;

&lt;p&gt;Our first step was to meet with everyone involved. The business technology team provided access to their repositories, internal hardware statistics, application metrics, and codebase to monitor and observe the application.&lt;/p&gt;

&lt;p&gt;After we had a chance to explore, read, and analyze the source code, we delivered our Tune Report, which provided a list of key areas that could be immediately addressed and others that could be improved upon over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Doing More with Less: Improved Server Performance Results in Reduced Costs
&lt;/h3&gt;

&lt;p&gt;Power’s applications run in a containerized environment. Their theory was that if they had more server instances and fewer processes running in those servers spread out over a wide area, that their application would be more resilient.&lt;/p&gt;

&lt;p&gt;However, our Tune Report revealed an excessive amount of memory available, compared to the amount of CPU they were able to use within their servers.&lt;/p&gt;

&lt;p&gt;Our recommendation was to reduce the number of server instances and increase the number of processes that each server executed.&lt;/p&gt;

&lt;p&gt;For example, instead of having 60 servers with 2 processes, it would be far more efficient to have 20 servers with 6 to 8 processes each.&lt;/p&gt;

&lt;p&gt;With more processes per server, there is less chance of one of those being busy when a request arrives. By making this adjustment, we were able to drive down the wait time from an average of 20-50 milliseconds to almost 0 milliseconds.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;With this, and all other changes made so far from the report, page loads were reduced from 5 seconds to below 3 seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;“This was a huge win for us because we were able to size our application accordingly across multiple server instances,” said Wade. “It only took a few days to test this and verify that they were right.”&lt;/p&gt;

&lt;p&gt;By running fewer server instances, hardware is freed up and computational power can be reallocated; ultimately, this results in cost savings and increased speed using half the server capacity.&lt;/p&gt;

&lt;p&gt;We also found ways to improve query performance by adding indexes in the database. “These were simple for us to add and immediately improved performance across our application,” said Wade.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Importance of Regular Checkups
&lt;/h3&gt;

&lt;p&gt;For Wade, the &lt;a href="https://fastruby.io/tune" rel="noopener noreferrer"&gt;Tune Report&lt;/a&gt; was like another set of eyes on areas they may not have thought to look at.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“It was great to have OmbuLabs/FastRuby.io’ insights. They came in, showed us where we could make improvements, and it was easy to implement their suggestions.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our team delivered a report that enabled Power to show noticeable results to non-technical stakeholders:&lt;/p&gt;

&lt;p&gt;“Our executives were thrilled with the report,” shared Wade. “They liked the fact that we could take action immediately to experience an almost instantaneous impact, and recommended that we invest in a Tune Report on a regular basis.”&lt;/p&gt;

&lt;p&gt;Wade felt the Tune Report was well worth the time and effort of getting an outside perspective and said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“I was shocked that we were able to do so much in a short amount of time and to show that kind of return on investment. The Tune Report came at a really great time and showed us how beneficial it was to do—not just once, but routinely. It’s like taking your application to the auto mechanic for a checkup.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;“I really enjoy working with OmbuLabs/FastRuby.io. They’re experts at what they do, and I’ve always been impressed with their skill, professionalism, and knowledge,” concluded Wade. “They are also very involved in the Ruby community which just adds to their credibility.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The collaboration between Power and OmbuLabs/FastRuby.io exemplifies how external experts can bolster in-house technological capabilities. The Tune Report proved an invaluable resource in optimizing Power’s Nitro platform, leading to improved performance and cost savings.&lt;/p&gt;

&lt;p&gt;The success of this collaboration underscores the importance of &lt;a href="https://fastruby.io/tune" rel="noopener noreferrer"&gt;regular performance audits&lt;/a&gt; and the value they bring in terms of enhancing user experience, reducing costs, and maintaining a competitive edge in a technology-driven world.&lt;/p&gt;

&lt;p&gt;The Tune Report, product of a collaboration between FastRuby.io and Speedshop, proved instrumental in identifying optimization opportunities, leading to a significant decrease in average page load time, from 5 to 3 seconds.&lt;/p&gt;

&lt;p&gt;One of the key recommendations, reducing the number of server instances and increasing the number of processes per server, resulted in a drop in wait time from 20-50 milliseconds to almost 0.&lt;/p&gt;

&lt;p&gt;If you are interested in hearing more about our &lt;a href="https://fastruby.io/tune" rel="noopener noreferrer"&gt;Ruby/Rails performance audits&lt;/a&gt;, &lt;a href="https://www.fastruby.io/tune#contactus" rel="noopener noreferrer"&gt;send us a quick message&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  WHO WE ARE:
&lt;/h3&gt;

&lt;p&gt;OmbuLabs is &lt;a href="https://www.ombulabs.com" rel="noopener noreferrer"&gt;Philadelphia's lean software boutique&lt;/a&gt;. Specializing in Ruby, &lt;a href="https://www.fastruby.io" rel="noopener noreferrer"&gt;Ruby/Rails Upgrades&lt;/a&gt; (FastRuby.io), JavaScript, &lt;a href="https://www.upgradejs.com" rel="noopener noreferrer"&gt;JavaScript Upgrades&lt;/a&gt; (UpgradeJS.com), minimum viable product development, and reducing technical debt, we help companies (from startups to Fortune 500 companies) build and improve their digital products.&lt;/p&gt;

&lt;p&gt;Founded in 2011 and headquartered in Philadelphia, Pennsylvania, our experienced and diverse team of developers is ready to do whatever it takes to help your business grow. &lt;/p&gt;

&lt;p&gt;To learn more, visit us at &lt;a href="https://www.ombulabs.com" rel="noopener noreferrer"&gt;OmbuLabs.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>performance</category>
    </item>
    <item>
      <title>4 Essential Security Tools To Level Up Your Rails Security</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Wed, 31 May 2023 11:23:45 +0000</pubDate>
      <link>https://dev.to/fastruby/4-essential-security-tools-to-level-up-your-rails-security-4ffe</link>
      <guid>https://dev.to/fastruby/4-essential-security-tools-to-level-up-your-rails-security-4ffe</guid>
      <description>&lt;p&gt;At FastRuby.io we love &lt;a href="https://rubyonrails.org" rel="noopener noreferrer"&gt;Ruby on Rails&lt;/a&gt; because it is so powerful: You can quickly create an application that is feature complete, stable, and &lt;a href="https://guides.rubyonrails.org/security.html" rel="noopener noreferrer"&gt;secure&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, maintaining a Rails application up to date and secure takes some effort.&lt;/p&gt;

&lt;p&gt;In this blog post, we will cover a few Ruby gems and best practices that you can use to stay on top of your security, reliability, and stability needs.&lt;/p&gt;

&lt;p&gt;Before we dive into 4 different Ruby gems you can use to improve your security support, let’s start with some of the security goodies that already come with Rails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rails’ Native Security Features
&lt;/h2&gt;

&lt;p&gt;The good news is that Rails comes with a ton of security support, here are a few features that you are probably already using in your application:&lt;/p&gt;

&lt;h3&gt;
  
  
  Forcing SSL
&lt;/h3&gt;

&lt;p&gt;You can easily secure your authentication by enabling TLS in your configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/environments/production.rb&lt;/span&gt;
&lt;span class="c1"&gt;# Force all access to the app over SSL,&lt;/span&gt;
&lt;span class="c1"&gt;# use Strict-Transport-Security,&lt;/span&gt;
&lt;span class="c1"&gt;# and use secure cookies&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;force_ssl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your web server (e.g. nginx) doesn’t force SSL, your Rails application will.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSRF (Cross Site Request Forgery)
&lt;/h3&gt;

&lt;p&gt;Some of these security features are enabled by default. For instance: &lt;a href="https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf" rel="noopener noreferrer"&gt;CSRF&lt;/a&gt;. Ruby on Rails has specific, built-in support for CSRF tokens.&lt;/p&gt;

&lt;p&gt;If you see this line in your &lt;code&gt;application_controller.rb&lt;/code&gt;, you know that you are including Rails’ protection against cross site request forgery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;protect_from_forgery&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CORS (Cross Origin Resource Sharing)
&lt;/h3&gt;

&lt;p&gt;Occasionally, a need arises to share resources with another domain. For example, a file-upload function that sends data via an AJAX request to another domain.&lt;/p&gt;

&lt;p&gt;In these cases, the same-origin rules followed by web browsers must be sent. Modern browsers, in compliance with HTML5 standards, will allow this to occur but in order to do this; some precaution must be taken.&lt;/p&gt;

&lt;p&gt;For this, you can use the &lt;a href="https://github.com/cyu/rack-cors" rel="noopener noreferrer"&gt;&lt;code&gt;rack-cors&lt;/code&gt;&lt;/a&gt; gem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rack-cors'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:require&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'rack/cors'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can configure it as a middleware like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/application.rb:&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Sample&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cors&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;origins&lt;/span&gt; &lt;span class="s1"&gt;'audit.fastruby.io'&lt;/span&gt;
        &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="sr"&gt;%r{/users/&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+.json}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;:headers&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Origin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Accept'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;:methods&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:get&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Filtering Parameters
&lt;/h3&gt;

&lt;p&gt;To make sure sensitive request parameters aren’t logged to your log files, you can set up your application like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter_parameters&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="ss"&gt;:credit_card_number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;:social_security_number&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Values for these parameters will now show up as &lt;code&gt;"[FILTERED]"&lt;/code&gt; in your log files.&lt;/p&gt;

&lt;p&gt;Now that we have covered some of the &lt;strong&gt;key security features in Rails&lt;/strong&gt; , let’s look at &lt;strong&gt;4 gems&lt;/strong&gt; that can help you level up your security best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bundler Audit&lt;/li&gt;
&lt;li&gt;Brakeman&lt;/li&gt;
&lt;li&gt;Rack::Attack&lt;/li&gt;
&lt;li&gt;Secure Headers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Bundler Audit
&lt;/h2&gt;

&lt;p&gt;This Ruby gem is quite useful for detecting versions of gems that are known to be vulnerable to security issues. &lt;a href="https://github.com/rubysec/bundler-audit" rel="noopener noreferrer"&gt;&lt;code&gt;bundler-audit&lt;/code&gt;&lt;/a&gt; uses an open database of vulnerable gems called &lt;a href="https://github.com/rubysec/ruby-advisory-db" rel="noopener noreferrer"&gt;&lt;code&gt;ruby-advisory-db&lt;/code&gt;&lt;/a&gt; and compares it to the versions that show up in your &lt;code&gt;Gemfile.lock&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can install it and run it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;bundler-audit
bundle-audit

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there are vulnerable versions in your &lt;code&gt;Gemfile.lock&lt;/code&gt; it will show you something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxxwqfj5ah8yyvmnfusd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpxxwqfj5ah8yyvmnfusd.png" alt="bundle-audit output for an app that has vulnerable dependencies" width="599" height="804"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If everything looks good, it will show you something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuf4lzjgoa6dyqlq6wngo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuf4lzjgoa6dyqlq6wngo.png" alt="bundle-audit output for an app that has no vulnerable dependencies" width="563" height="126"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few years ago we built a small, open source, web application to make it easier for us to share these vulnerabilities reports with our clients. You can find it and use it over here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://audit.fastruby.io" rel="noopener noreferrer"&gt;Bundler Audit Tool&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It usually looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11b6azn5trdd2g5j7ho4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11b6azn5trdd2g5j7ho4.png" alt="bundle-audit results in the open source application hosted by FastRuby.io" width="680" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bundler-audit&lt;/code&gt; relies on &lt;code&gt;ruby-advisory-db&lt;/code&gt;, a community-maintained database of known security vulnerabilities. While this is a great approach, it may also lead to gaps in coverage if a vulnerability is not yet documented in the database or if the database becomes outdated.&lt;/p&gt;

&lt;p&gt;If you want to make sure that &lt;code&gt;bundler-audit&lt;/code&gt; is running with the latest version of the &lt;code&gt;ruby-advisory-db&lt;/code&gt;, you should always call it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle-audit check --update

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Brakeman
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://brakemanscanner.org/" rel="noopener noreferrer"&gt;&lt;code&gt;brakeman&lt;/code&gt;&lt;/a&gt; is another useful Ruby gem that is a static analysis security vulnerability scanner for Ruby on Rails applications.&lt;/p&gt;

&lt;p&gt;You can quickly run it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd path/to/rails-app
gem install brakeman
brakeman

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will run a battery of “checks” against your source code and report any potential security issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loading scanner...
Processing application in /Users/etagwerker/Projects/ombulabs/foobar
Processing gems...
[Notice] Detected Rails 6 application
Processing configuration...
[Notice] Escaping HTML by default
Parsing files...
Detecting file types...
Processing initializers...
Processing libs...
Processing routes...
Processing templates...
Processing data flow in templates...
Processing models...
Processing controllers...
Processing data flow in controllers...
Indexing call sites...
Running checks in parallel...
 - CheckBasicAuth
 ...
 - CheckYAMLParsing
Checks finished, collecting results...
Generating report...

== Brakeman Report ==

Application Path: /Users/etagwerker/Projects/ombulabs/foobar
Rails Version: 6.1.4
Brakeman Version: 5.4.1
Scan Date: 2023-05-02 15:18:55 -0400
Duration: 0.430032 seconds
Checks Run: BasicAuth, ..., YAMLParsing

== Overview ==

Controllers: 3
Models: 2
Templates: 15
Errors: 0
Security Warnings: 2

== Warning Types ==

Cross-Site Scripting: 1
Unmaintained Dependency: 1

== Warnings ==

Confidence: High
Category: Unmaintained Dependency
Check: EOLRuby
Message: Support for Ruby 2.7.2 ended on 2023-03-31
File: .ruby-version
Line: 1

Confidence: Weak
Category: Cross-Site Scripting
Check: SanitizeConfigCve
Message: rails-html-sanitizer 1.3.0 is vulnerable to cross-site scripting when `select` and `style` tags are allowed (CVE-2022-32209). Upgrade to 1.4.3 or newer
File: Gemfile.lock
Line: 194

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see a complete list of checks ran by Brakeman, you can find them over here: &lt;a href="https://github.com/presidentbeef/brakeman/tree/main/lib/brakeman/checks" rel="noopener noreferrer"&gt;List of Brakeman Checks&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This sample application is in pretty good shape, but it does report two warnings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;High: I’m using Rails 6.1 with Ruby 2.7.2. &lt;a href="https://twitter.com/yukihiro_matz/status/1639758019235053571" rel="noopener noreferrer"&gt;Security support for Ruby 2.7.x ended on March 31st, 2023&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Weak: I’m using a version of &lt;a href="https://rubygems.org/gems/rails-html-sanitizer" rel="noopener noreferrer"&gt;&lt;code&gt;rails-html-sanitizer&lt;/code&gt;&lt;/a&gt; that is known to be vulnerable.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of them is quite easy to fix: &lt;code&gt;bundle update rails-html-sanitizer&lt;/code&gt; and the warning goes away. We can safely deploy this to production with a small pull request.&lt;/p&gt;

&lt;p&gt;The other warning might be harder to accomplish: We need to &lt;a href="https://www.fastruby.io/blog/ruby/upgrades/upgrade-ruby-from-2.7-to-3.0.html" rel="noopener noreferrer"&gt;upgrade from Ruby 2.7 to 3.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Just like any other static analysis tool, &lt;code&gt;brakeman&lt;/code&gt; may generate false positives or negatives. This means that it might report issues that aren’t actually problems or fail to detect actual vulnerabilities. We need to be cautious when interpreting the results and not rely solely on these tools for ensuring security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rack::Attack
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rack-attack&lt;/code&gt; is a middleware that can be used in your Rails application (and any rack-based Ruby applications!) to protect it from &lt;em&gt;bad clients&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Rack::Attack&lt;/code&gt; lets you easily decide when to allow, block and throttle based on properties of the request.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here are some of its key features:&lt;/p&gt;

&lt;h3&gt;
  
  
  Safelisting / Blocklisting
&lt;/h3&gt;

&lt;p&gt;You can quickly allow an IP like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/rack_attack.rb (for rails app)&lt;/span&gt;
&lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safelist_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"5.6.7.0/24"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This could be useful if you want to allowlist your development team’s IP addresses.&lt;/p&gt;

&lt;p&gt;You could quickly block an IP like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/rack_attack.rb (for rails apps)&lt;/span&gt;
&lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blocklist_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"1.2.3.4"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s a powerful combination to combine &lt;code&gt;rack-attack&lt;/code&gt; with environment variables like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/rack-attack.rb&lt;/span&gt;

&lt;span class="n"&gt;bad_ips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"BLOCKLIST_IPS"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# to be blocked&lt;/span&gt;

&lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blocklist&lt;/span&gt; &lt;span class="s2"&gt;"blocklist bad IP address"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;bad_ips&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;good_ips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SAFELIST_IPS"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# to be safelisted&lt;/span&gt;

&lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safelist&lt;/span&gt; &lt;span class="s2"&gt;"safelisted good IP address"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;good_ips&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Throttling
&lt;/h3&gt;

&lt;p&gt;This would be a good way to slow down an attacker who is trying to sign in to a user account using a &lt;a href="https://www.kaspersky.com/resource-center/definitions/brute-force-attack" rel="noopener noreferrer"&gt;brute force attack&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Throttle login attempts for a given email parameter to 6 reqs/minute&lt;/span&gt;
&lt;span class="c1"&gt;# Return the *normalized* email as a discriminator on POST /login requests&lt;/span&gt;
&lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'limit logins per email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;period: &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'/login'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post?&lt;/span&gt;
    &lt;span class="c1"&gt;# Normalize the email, using the same logic as your authentication process, to&lt;/span&gt;
    &lt;span class="c1"&gt;# protect against rate limit bypasses.&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\s+/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Additionally, you could have a more broad criteria for limiting the number of requests per minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/initializers/rack_attack.rb (for rails apps)&lt;/span&gt;

&lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"requests by ip"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;period: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Based on the IP of the user, this configuration would limit their rate to 5 requests every 2 seconds.&lt;/p&gt;

&lt;p&gt;You can combine &lt;code&gt;rack-attack&lt;/code&gt; and &lt;a href="https://www.cloudflare.com" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt; as a good way to prevent &lt;a href="https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/" rel="noopener noreferrer"&gt;DDoS attacks&lt;/a&gt;. Just because you are using &lt;code&gt;rack-attack&lt;/code&gt;, it doesn’t mean that you should not use a tool like Cloudflare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secure Headers
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/github/secure_headers" rel="noopener noreferrer"&gt;&lt;code&gt;secure_headers&lt;/code&gt;&lt;/a&gt; gem will automatically apply several headers that are related to security. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content Security Policy (CSP)&lt;/strong&gt; - Helps detect/prevent XSS, mixed-content, and other classes of attack. Default value: &lt;code&gt;default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;HTTP Strict Transport Security (HSTS)&lt;/strong&gt; - Protects from SSLStrip/Firesheep attacks. Default value: &lt;code&gt;max-age=631138519&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;X-Frame-Options (XFO)&lt;/strong&gt; - Prevents your content from being framed and potentially clickjacked. Default value: &lt;code&gt;sameorigin&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;X-XSS-Protection&lt;/strong&gt; - Cross site scripting heuristic filter for IE/Chrome. Default value: &lt;code&gt;1; mode=block&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;X-Content-Type-Options&lt;/strong&gt; - Prevent content type sniffing. Default value: &lt;code&gt;nosniff&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;X-Download-Options&lt;/strong&gt; - Prevent file downloads opening. Default value: &lt;code&gt;noopen&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;X-Permitted-Cross-Domain-Policies&lt;/strong&gt; - Restrict Adobe Flash Player’s access to data. Default value: &lt;code&gt;none&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While &lt;code&gt;secure_headers&lt;/code&gt; provides sensible safe default settings, it is essential to tailor them to your application’s specific needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;p&gt;If you’re interested in reading more about securing your Rails applications, here is a list of useful resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html" rel="noopener noreferrer"&gt;OWASP’s Ruby on Rails Cheat Sheet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://groups.google.com/g/rubyonrails-security" rel="noopener noreferrer"&gt;Ruby on Rails Security Email List&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/presidentbeef/brakeman" rel="noopener noreferrer"&gt;Brakeman&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://groups.google.com/g/ruby-security-ann" rel="noopener noreferrer"&gt;Ruby Security Email List&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rack/rack-attack" rel="noopener noreferrer"&gt;Rack::Attack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rubysec/bundler-audit" rel="noopener noreferrer"&gt;Bundler::Audit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wafris.org/guides/ultimate-guide-to-rack-attack" rel="noopener noreferrer"&gt;Ultimate Guide to Rack::Attack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/pxlpnk/awesome-ruby-security" rel="noopener noreferrer"&gt;Awesome Ruby Security Resources&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While Rails provides a lot of security features, there are at least 4 ways to level up your application’s security with these great tools:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;bundler-audit&lt;/code&gt; to find dependencies that are known to have vulnerabilities&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;brakeman&lt;/code&gt; to find idioms/calls that could be dangerous for your application&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rack-attack&lt;/code&gt; to defend your application against bad, abusive clients&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;secure_headers&lt;/code&gt; to quickly apply several security headers to all your responses&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This list is by no means exhaustive but it shows some of the key tools you can use to stay on top of potential security issues.&lt;/p&gt;

&lt;p&gt;Although tools like &lt;code&gt;bundler-audit&lt;/code&gt; and &lt;code&gt;brakeman&lt;/code&gt; are helpful in identifying potential vulnerabilities, &lt;strong&gt;they should not be solely relied upon&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As a team, you should define a &lt;strong&gt;comprehensive security strategy&lt;/strong&gt; which includes regular code audits, code reviews, and penetration testing.&lt;/p&gt;

&lt;p&gt;Happy hacking!&lt;/p&gt;

&lt;p&gt;PS: &lt;a href="https://fastruby.io" rel="noopener noreferrer"&gt;Need a code security audit? Contact us!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>security</category>
    </item>
    <item>
      <title>Introduction to Rails Engines</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Tue, 23 May 2023 18:58:06 +0000</pubDate>
      <link>https://dev.to/fastruby/introduction-to-rails-engines-4ilj</link>
      <guid>https://dev.to/fastruby/introduction-to-rails-engines-4ilj</guid>
      <description>&lt;p&gt;Rails Engines are an architectural pattern that can be used to modularize a Rails application. They are self-contained applications that can be mounted within a larger Rails application. In this post, we will dive into the world of Rails Engines and explore what they are, how to create them, how to use them, when to use them, and why they are important.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Rails Engines?
&lt;/h2&gt;

&lt;p&gt;Rails Engines are essentially mini-applications that can be plugged into a larger Rails application. They allow you to modularize your code and keep it separate from the core application. This makes it easier to maintain and update the codebase as a whole.&lt;/p&gt;

&lt;p&gt;Engines can include models, controllers, views, and assets, and they can also have their own routes and configurations. For example &lt;a href="https://github.com/plataformatec/devise" rel="noopener noreferrer"&gt;Devise&lt;/a&gt;, an engine that provides authentication for a host application, has its own views, controllers, routes and configurations.&lt;/p&gt;

&lt;p&gt;It is worth mentioning that Rails Engines are similar to &lt;a href="https://api.rubyonrails.org/classes/Rails/Railtie.html" rel="noopener noreferrer"&gt;Railities&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;However, they differ in that Railities are simple Ruby modules that includes various hooks into the Rails initialization process, such as configuring middleware or adding new generators, whereas a Rails Engine has a more complex directory structure and includes everything needed to function as a standalone application, such as controllers, models, and views.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Create a Rails Engine
&lt;/h2&gt;

&lt;p&gt;Creating a Rails engine is a relatively simple process. To create a new engine, run the following command in the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rails plugin new payments --mountable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create a new Rails engine in the &lt;code&gt;payments&lt;/code&gt; directory with the &lt;code&gt;--mountable&lt;/code&gt; option, which means that the engine can be mounted within another Rails application. Please note that the &lt;code&gt;--mountable&lt;/code&gt; option is very important as running the above command without it will create a typical plugin which is not mountable. Below are the files generated by the above command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ bin/rails plugin new payments --mountable
      create  
      create README.md
      create Rakefile
      create payments.gemspec
      create .gitignore
      create Gemfile
         run git init from "."
Initialized empty Git repository in /ombulabs.com/payments/.git/
      create app
      create app/controllers/payments/application_controller.rb
      create app/helpers/payments/application_helper.rb
      create app/jobs/payments/application_job.rb
      create app/mailers/payments/application_mailer.rb
      create app/models/payments/application_record.rb
      create app/views/layouts/payments/application.html.erb
      create app/assets/images/payments
      create app/assets/images/payments/.keep
      create app/models/concerns
      create app/models/concerns/.keep
      create app/controllers/concerns
      create app/controllers/concerns/.keep
      create config/routes.rb
      create lib/payments.rb
      create lib/tasks/payments_tasks.rake
      create lib/payments/version.rb
      create lib/payments/engine.rb
      create app/assets/config/payments_manifest.js
      create app/assets/stylesheets/payments/application.css
      create bin/rails
      create test/test_helper.rb
      create test/payments_test.rb
      create test/fixtures/files
      create test/fixtures/files/.keep
      create test/controllers
      create test/controllers/.keep
      create test/mailers
      create test/mailers/.keep
      create test/models
      create test/models/.keep
      create test/integration
      create test/integration/.keep
      create test/helpers
      create test/helpers/.keep
      create test/integration/navigation_test.rb
  vendor_app test/dummy
      append /ombulabs.com/Gemfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you have created your engine, you can add models, controllers, views, and assets just like you would in a regular Rails application. You can also define your own routes and configurations within the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Use Rails Engines
&lt;/h2&gt;

&lt;p&gt;Using a Rails Engine is also straightforward. The engine needs to be specified inside the &lt;code&gt;Gemfile&lt;/code&gt; and run &lt;code&gt;bundle install&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'payments'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;path: &lt;/span&gt;&lt;span class="s1"&gt;'payments'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make our &lt;code&gt;Payments&lt;/code&gt; engine’s functionality accessible from within our application, we need to mount it in the host application’s &lt;code&gt;config/routes.rb&lt;/code&gt; file. However, it is not always the case as we can use the engine functionality without mounting it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;mount&lt;/span&gt; &lt;span class="no"&gt;Payments&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;at: &lt;/span&gt;&lt;span class="s2"&gt;"/payments"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will mount our &lt;code&gt;Payments&lt;/code&gt; engine at the specified path within your application. You can now use the models, controllers, views, and assets from the engine just like you would in a regular Rails application.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to Use Rails Engines
&lt;/h2&gt;

&lt;p&gt;Rails Engines are particularly useful when you want to modularize your codebase and keep it separate from the core application. This is especially helpful when you are working on a large project with multiple developers, as it allows you to separate responsibilities and avoid conflicts.&lt;/p&gt;

&lt;p&gt;Engines can also be used to create reusable components that can be shared across multiple applications. For example, in an e-commerce context, you could use Rails Engines to build separate modules for different aspects of the site, such as product catalog, shopping cart, payment gateway, and order management.&lt;/p&gt;

&lt;p&gt;Each module could be developed and tested independently and then mounted within the main Rails application. This approach would allow you to scale each module independently as needed, and also make it easier to reuse the code across different e-commerce sites.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rails Engines are Important
&lt;/h2&gt;

&lt;p&gt;Rails Engines are important because they provide a modular way to organize and share code within a larger Rails application. Here are a few key benefits of using Rails engines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Code Reuse: Rails engines allow you to extract reusable code into separate modules, making it easier to share code across multiple projects. This can save time and effort in development, as you can develop and test functionality once and then reuse it in different contexts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modularity: By breaking up your Rails application into smaller, self-contained modules, you can make your codebase more manageable and easier to maintain. Each engine can be developed and tested independently, allowing you to focus on specific areas of functionality without worrying about breaking changes in the rest of the application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Encapsulation: Engines provide a way to encapsulate functionality within a well-defined interface. This makes it easier to reason about the code and reduces the risk of introducing bugs or breaking changes in other parts of the application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scalability: Engines can be used to scale your application horizontally by allowing you to add or remove functionality as needed. This can help you to adapt your application to changing business requirements and user needs without having to make major changes to the overall architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Customization: Rails engines can be customized to meet the specific needs of different projects or clients. This can be particularly useful in agency or consultancy contexts, where you may need to develop similar functionality across multiple projects but with slight variations in requirements.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By keeping your code organized and modularized, you can improve the overall quality and maintainability of your application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In conclusion, Rails engines provide a powerful way to organize and share code within a larger Rails application. By breaking up your application into smaller, self-contained modules, you can make your codebase more manageable and easier to maintain. Engines allow you to extract reusable code, encapsulate functionality, and scale your application horizontally by adding or removing functionality as needed. Additionally, engines can be customized to meet the specific needs of different projects or clients.&lt;/p&gt;

&lt;p&gt;While Rails engines may not be appropriate for every project or context, they can be a valuable tool for developers looking to create more modular, maintainable, and scalable applications. Whether you’re building an HR management system, an e-commerce site, or a content management system, Rails engines can help you to develop and reuse functionality in a more efficient and effective way.&lt;/p&gt;

&lt;p&gt;For further reading you can go through the &lt;a href="https://guides.rubyonrails.org/engines.html" rel="noopener noreferrer"&gt;Rails guide for Engines&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Need help modularising your Rails application? &lt;a href="https://www.fastruby.io/#contactus" rel="noopener noreferrer"&gt;Contact us&lt;/a&gt; and have the &lt;a href="https://www.fastruby.io/" rel="noopener noreferrer"&gt;FastRuby.io experts&lt;/a&gt; help you with it!&lt;/p&gt;

</description>
      <category>rails</category>
    </item>
    <item>
      <title>Fixed-cost, Monthly Maintenance Services</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Tue, 23 May 2023 10:23:45 +0000</pubDate>
      <link>https://dev.to/fastruby/fixed-cost-monthly-maintenance-services-371b</link>
      <guid>https://dev.to/fastruby/fixed-cost-monthly-maintenance-services-371b</guid>
      <description>&lt;p&gt;Ever since we started offering &lt;a href="https://fastruby.io" rel="noopener noreferrer"&gt;productized Ruby and Rails upgrade services&lt;/a&gt; and &lt;a href="https://fastruby.io/roadmap" rel="noopener noreferrer"&gt;upgrade roadmaps&lt;/a&gt;, we’ve been interested in helping as many people and companies as possible.&lt;/p&gt;

&lt;p&gt;Unfortunately, in the past we’ve had to turn down companies who wanted to work with us but couldn’t secure the minimum monthly budget to work with our experts.&lt;/p&gt;

&lt;p&gt;I’m pleased to announce that we’re now offering new opportunities for startups and small businesses to work alongside our team.&lt;/p&gt;

&lt;p&gt;In this article, I will share a few new options to collaborate with our team of experts who specialize in &lt;a href="https://fastruby.io/blog/tags/tech-tebt" rel="noopener noreferrer"&gt;technical debt&lt;/a&gt; remediation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;Recently I had the opportunity to connect with a lot of SaaS founders at &lt;a href="https://microconf.com/" rel="noopener noreferrer"&gt;MicroConf&lt;/a&gt; ‘23 which made me realize that there are many entrepreneurs, startups, and small businesses out there that want to pay off technical debt by investing a small amount of money per month.&lt;/p&gt;

&lt;p&gt;While teams understand the need to remediate their technical debt, this very often loses priority to shipping features, releasing patches, and addressing other tasks prioritized in their product roadmaps.&lt;/p&gt;

&lt;p&gt;Although everybody would like to upgrade their app as fast as possible, sometimes that’s just not in the budget. Instead, a good alternative can be to gradually work through your technical debt with a slower-paced retainer model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's included in our monthly maintenance services
&lt;/h2&gt;

&lt;p&gt;Our fixed-cost, monthly retainer will include services to gradually reduce the technical debt in your application. Our services will include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ruby Upgrades&lt;/li&gt;
&lt;li&gt;Rails Upgrades&lt;/li&gt;
&lt;li&gt;Dependency Management&lt;/li&gt;
&lt;li&gt;Performance Monitoring&lt;/li&gt;
&lt;li&gt;Tech Debt Management&lt;/li&gt;
&lt;li&gt;Security Patches&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ruby Upgrades
&lt;/h3&gt;

&lt;p&gt;If your application could use a Ruby upgrade, we will submit a pull request that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuia3tjo1jll0iht8bhy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcuia3tjo1jll0iht8bhy.png" alt="Pull request to upgrade Ruby to 3.2" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Source: &lt;a href="https://github.com/fastruby/points/pull/290/files" rel="noopener noreferrer"&gt;Upgrade to Ruby 3.2 on Points&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;If your application is not properly configured to &lt;a href="https://www.fastruby.io/blog/upgrade-rails/dual-boot/dual-boot-with-rails-6-0-beta.html" rel="noopener noreferrer"&gt;dual boot&lt;/a&gt; with &lt;a href="https://github.com/fastruby/next_rails" rel="noopener noreferrer"&gt;&lt;code&gt;next_rails&lt;/code&gt;&lt;/a&gt;, we will submit a PR that looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdldpfzbcz3qw3zfp2y2r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdldpfzbcz3qw3zfp2y2r.png" alt="Pull request to set up dual booting in a Gemfile" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Source: &lt;a href="https://github.com/fastruby/benchmark.fyi/pull/8/files" rel="noopener noreferrer"&gt;Set up dual booting on Benchmark.fyi&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;This will be the basis of our work around dependency management.&lt;/p&gt;

&lt;p&gt;We will make sure that your application can run with both versions of Ruby. If you are on Ruby 2.7, we will make sure that your test suite works with both &lt;a href="https://www.fastruby.io/blog/ruby/upgrades/upgrade-ruby-from-2.7-to-3.0.html" rel="noopener noreferrer"&gt;Ruby 2.7 and 3.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once we fix all the errors in your test suite, we will make sure that your staging environment works with the next version of Ruby. If anything goes wrong, we will make sure we patch all issues before we deploy the change to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rails Upgrades
&lt;/h3&gt;

&lt;p&gt;Similarly, we will gradually tackle Rails upgrade projects. We will use our years of experience to submit many, tiny pull requests that will upgrade your application taking baby steps.&lt;/p&gt;

&lt;p&gt;We will make sure that your application can run with both versions of Rails. If you are on Rails 5.2, we will make sure that your test suite works with both &lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-5-2-to-6-0.html" rel="noopener noreferrer"&gt;Rails 5.2 and 6.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once we fix all the errors in your test suite, we will make sure that your staging environment works with the next version of Rails. If anything goes wrong, we will make sure we patch all issues before we deploy the change to production.&lt;/p&gt;

&lt;p&gt;If you are curious about the dual booting technique, we have been running the Rails upgrade workshop at &lt;a href="https://railsconf.org/" rel="noopener noreferrer"&gt;RailsConf&lt;/a&gt; since 2019. Here is a recording of the workshop we delivered at &lt;a href="https://www.youtube.com/watch?v=Sid39aN_SKk" rel="noopener noreferrer"&gt;RailsConf 2021&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency Management
&lt;/h3&gt;

&lt;p&gt;While there are great tools out there that can quickly help you upgrade minor versions of your dependencies (e.g. &lt;a href="https://depfu.com/" rel="noopener noreferrer"&gt;Depfu&lt;/a&gt;), there seems to be a missing link between what we do (&lt;a href="https://www.fastruby.io/" rel="noopener noreferrer"&gt;Ruby/Rails Upgrades&lt;/a&gt;) and what they do (shipping automated pull requests, upgrading one minor version at a time).&lt;/p&gt;

&lt;p&gt;That’s where we come in. We will work on minor and major version upgrades (&lt;a href="https://semver.org/" rel="noopener noreferrer"&gt;according to semver&lt;/a&gt;) for all of your dependencies.&lt;/p&gt;

&lt;p&gt;We will submit one PR at a time. This will give us the opportunity to properly test, deploy, and monitor any issues in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Monitoring
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsyucwgkgrou42rxliu19.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsyucwgkgrou42rxliu19.png" alt="Sample of a screenshot of NewRelic" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If your application is not currently using an Application Performance Monitoring (APM) service (e.g. &lt;a href="https://newrelic.com/" rel="noopener noreferrer"&gt;New Relic&lt;/a&gt;), we will set one up for you. We’ll consider your needs and budget, to make the best recommendation.&lt;/p&gt;

&lt;p&gt;Once your APM is up and running, we will keep track of low performance and high memory load issues. We will share a monthly report with the top issues we noticed as &lt;a href="https://www.fastruby.io/blog/tags/performance" rel="noopener noreferrer"&gt;Ruby and Rails performance experts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you want a more in-depth analysis of your application’s performance, we can deliver our highly-rate and always insightful &lt;a href="https://www.fastruby.io/tune" rel="noopener noreferrer"&gt;Tune Report&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;While the Tune Report is not included in any of the monthly plans, our clients have found that they can recover their investment in about 3 months.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm04pc94o4wvz6raaksu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm04pc94o4wvz6raaksu.png" alt="Screenshot of a sample Tune Report, Rails performance optimization audit provided by FastRuby.io" width="600" height="309"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tech Debt Management
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdu1bnhme1oo18o5h7jl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkdu1bnhme1oo18o5h7jl.png" alt="Screenshot of a sample rubycritic report of the Points open source Rails application" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As open source maintainers of technical debt projects like  &lt;a href="https://github.com/whitesmith/rubycritic" rel="noopener noreferrer"&gt;RubyCritic&lt;/a&gt; and  &lt;a href="https://github.com/fastruby/skunk" rel="noopener noreferrer"&gt;Skunk&lt;/a&gt; we know how important it is to keep track of your code complexity, churn, and code coverage metrics.&lt;/p&gt;

&lt;p&gt;On top of that, we know how difficult it is to communicate the problems with consistently increasing technical debt:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduced development velocity&lt;/li&gt;
&lt;li&gt;Constant regressions&lt;/li&gt;
&lt;li&gt;Undesired side effects when deploying small changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can help with that! Every month you will get a report on your technical debt, not just on the codebase, but also in your development workflow, infrastructure, and best practices.&lt;/p&gt;

&lt;p&gt;In this report we will include any changes that were made to simplify complex code structures, enhance your development workflow, or anything else that was shipped to make your codebase easier to maintain.&lt;/p&gt;

&lt;p&gt;Our goal is to gradually improve &lt;a href="https://fastruby.io/blog/tags/code-quality" rel="noopener noreferrer"&gt;code quality&lt;/a&gt; without disrupting your development team’s operations.&lt;/p&gt;

&lt;p&gt;If you don’t have a shared understanding of what “code quality” really is, we can help you define one that works for your team. After that we can help you “codify” your code quality definition with tools like &lt;code&gt;reek&lt;/code&gt;, &lt;code&gt;rubocop&lt;/code&gt;, and&lt;code&gt;rubycritic&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Patches
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11b6azn5trdd2g5j7ho4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11b6azn5trdd2g5j7ho4.png" alt="bundle-audit results in the open source application hosted by FastRuby.io" width="680" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Picture this: On day one of our engagement our team will perform a quick security assessment of your codebase. This will give us an idea of the work to be done to secure your application from known exploits and potential attack vectors.&lt;/p&gt;

&lt;p&gt;Every month we will make security patches that will &lt;a href="https://www.fastruby.io/blog/rails/security/ruby-security-toolkit.html" rel="noopener noreferrer"&gt;level up the security standards for your Rails application&lt;/a&gt;. Depending on the severity of the issues we find, we might prioritize this topic on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;p&gt;Are you ready to make your application more maintainable?&lt;/p&gt;

&lt;p&gt;Considering our 10+ years of experience upgrading applications, and more than &lt;a href="https://twitter.com/etagwerker/status/1658189454715019264?s=20" rel="noopener noreferrer"&gt;20,000 developer hours paying off technical debt&lt;/a&gt;, we believe we can offer meaningful updates to your codebase starting at $1,000 per month:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shito&lt;/strong&gt; : $1,000 per month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shohin&lt;/strong&gt; : $2,000 per month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Komono&lt;/strong&gt; : $3,000 per month&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each package includes a maximum number of hours per month that we can invest in your application. When naming these packages, we decided to go with Bonsai types because we see a lot of similarities between pruning Bonsai trees and gradually paying off technical debt. 🌳&lt;/p&gt;

&lt;h3&gt;
  
  
  Shito
&lt;/h3&gt;

&lt;p&gt;This is a great package for organizations and companies that either don’t have a lot of technical debt or want to go slow.&lt;/p&gt;

&lt;p&gt;Their test suite might be well-written, their Rails application might be a small monolith, or their development team would be overwhelmed with more than 2 pull requests per month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shohin
&lt;/h3&gt;

&lt;p&gt;This is a great package for organizations and companies that have some technical debt, know they need to fix it, and are ready to review from 2 to 4 pull requests per month.&lt;/p&gt;

&lt;p&gt;Their test suite might need some attention, their Rails application might have between 50 and 150 models, or they’re one or two versions behind the latest stable release of Ruby and Rails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Komono
&lt;/h3&gt;

&lt;p&gt;This is a great package for organizations and companies that are ready to ship 3x more than if they had chosen the Shito package.&lt;/p&gt;

&lt;p&gt;Usually their Rails application is rather large, their main dependencies (Ruby and Rails) are a few versions behind, their test suite is flaky, or they need to move fast and upgrade things.&lt;/p&gt;

&lt;p&gt;Depending on the plan that you pick, we will invest from 5 to 20 hours per month remediating technical debt in your application/s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategic Projects
&lt;/h2&gt;

&lt;p&gt;We know that every now and then our clients will have one-off, strategic projects they will want to ship as soon as possible and we want to be able to help.&lt;/p&gt;

&lt;p&gt;When that happens, we can offer our services on an hourly basis to ship value in the form of features, infrastructure upgrades, bug fixes, or anything that will make &lt;a href="https://www.fastruby.io/blog/tags/case-study" rel="noopener noreferrer"&gt;a difference in our clients’ businesses&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rescue Services
&lt;/h2&gt;

&lt;p&gt;Our monthly packages don’t include rescue services. If your application needs to be rescued, we can certainly help! We’ve done this many times in the past and we are happy to get you out of a thorny situation.&lt;/p&gt;

&lt;p&gt;For these projects, we need to start with a short, one-week retainer to kickstart the rescue project. This will give us time to assess the situation, come up with a rescue plan, and start executing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;We are used to working with teams of all sizes. From engineering teams of one to teams of 50+ software engineers, we see every engagement as a successful collaboration between our team and your team.&lt;/p&gt;

&lt;p&gt;We thrive when we collaborate with our clients’ engineering team on a daily basis, so we will have a channel to communicate asynchronously. If you need to get on a call, we can do that too!&lt;/p&gt;

&lt;p&gt;Any of the changes that we submit will be open for review and discussion. We will clearly explain why they are necessary, what the changes will do, and what impact they will have in your &lt;a href="https://www.fastruby.io/blog/code-quality/calculate-tech-debt-using-skunk-on-github-actions.html" rel="noopener noreferrer"&gt;tech debt&lt;/a&gt; scorecard.&lt;/p&gt;

&lt;p&gt;Communication is key to us! We provide you with a monthly report on your technical debt, including codebase, development workflow, infrastructure, and best practices. We are always open to hearing your thoughts and needs and answering your questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Our commitment to helping people and businesses overcome their technical debt is stronger than ever.&lt;/p&gt;

&lt;p&gt;We are excited to launch our new monthly maintenance packages tailored for non-profit organizations, startups and small businesses. We are certain we will gradually reduce technical debt in their applications.&lt;/p&gt;

&lt;p&gt;Our technical debt remediation services, provided by a team of experts, include dependency management, Ruby upgrades, Rails upgrades,  performance monitoring, tech debt management, and security patches.&lt;/p&gt;

&lt;p&gt;These three affordable plans starting at $1,000 per month are cashflow-friendly, consistently add value to your codebase, and give you access to our team of &lt;a href="https://www.fastruby.io/blog/upgrade-rails/legacy-rails-silently-judging-you.html" rel="noopener noreferrer"&gt;legacy code&lt;/a&gt; experts.&lt;/p&gt;

&lt;p&gt;You can expect to see gradual improvements in your &lt;a href="https://www.fastruby.io/blog/tags/code-quality" rel="noopener noreferrer"&gt;code quality&lt;/a&gt; and overall decreasing technical debt while keeping your development team’s velocity uninterrupted.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The typical development organization can increase their feature delivery efficiency by at least 25% by managing technical debt. That’s the equivalent of having &lt;strong&gt;25% more developers without additional staffing costs or&lt;/strong&gt;** coordination needs**.”&lt;/p&gt;

&lt;p&gt;(Source: &lt;a href="https://codescene.com/hubfs/calculate-business-costs-of-technical-debt.pdf" rel="noopener noreferrer"&gt;CodeScene Whitepaper&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What would you do if you had 25% more development efficiency? How much more could your development team deliver in a month?&lt;/p&gt;

&lt;p&gt;Let us be your trusted partner in making your application more maintainable, secure, and efficient.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.fastruby.io/#contactus" rel="noopener noreferrer"&gt;Ready to pay off technical debt? Send us a message!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>maintenance</category>
    </item>
    <item>
      <title>7 Common Mistakes in Rails Upgrades</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Tue, 25 Apr 2023 00:30:00 +0000</pubDate>
      <link>https://dev.to/etagwerker/7-common-mistakes-in-rails-upgrades-3m3a</link>
      <guid>https://dev.to/etagwerker/7-common-mistakes-in-rails-upgrades-3m3a</guid>
      <description>&lt;p&gt;(This article was written by &lt;a href="http://twitter.com/jainrishi15/status/1650507041012604941" rel="noopener noreferrer"&gt;Rishi Jain&lt;/a&gt; and originally published in the &lt;a href="https://www.fastruby.io/blog" rel="noopener noreferrer"&gt;FastRuby.io Blog&lt;/a&gt;) &lt;/p&gt;

&lt;p&gt;&lt;a href="https://rubyonrails.org" rel="noopener noreferrer"&gt;Ruby on Rails&lt;/a&gt; is a popular web application framework that is constantly evolving with new versions being released frequently. While upgrading to a newer Rails version can bring new features, better performance, and security patches/improvements, it can also be a challenging task.&lt;/p&gt;

&lt;p&gt;In this blog post, we will discuss 7 common mistakes made while doing &lt;a href="https://www.fastruby.io/blog/rails/upgrade/rails-upgrade-series.html" rel="noopener noreferrer"&gt;Rails upgrades&lt;/a&gt;and how to avoid them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skipping Versions
&lt;/h2&gt;

&lt;p&gt;We have seen customers trying to upgrade Rails from 5.2 to 7.0 and Ruby from 2.5 to 3.1 all under one single pull request and failing to deploy a successful version on production.&lt;/p&gt;

&lt;p&gt;The problem with this approach is that it becomes extremely difficult to point out where the problem lies when your user encounters a bug in the app.&lt;/p&gt;

&lt;p&gt;Did it happen when you upgraded from &lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-5-2-to-6-0.html" rel="noopener noreferrer"&gt;Rails 5.2 to 6.0&lt;/a&gt; or in any other jump? Did it happen due to a deprecation warning that was supposed to be handled in &lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-6-0-to-6-1.html" rel="noopener noreferrer"&gt;Rails 6.0 to 6.1&lt;/a&gt; jump but you skipped that as you tried to upgrade directly from Rails 5.2 to 7.0?&lt;/p&gt;

&lt;p&gt;Another problem here is that you miss the chance to achieve small wins by deploying relatively small upgrades to production and thus exposing yourself to greater chances of encountering bugs and downtime to your app.&lt;/p&gt;

&lt;p&gt;The only way to avoid these problems is to take a step-wise approach of upgrading only one major or one minor version at a time. If we were to upgrade an app from Rails 5.2 to 7.0, we would first upgrade it to Rails 6.0, deploy to production, then upgrade to 6.1, deploy to production, and then upgrade to 7.0 and deploy to production.&lt;/p&gt;

&lt;p&gt;The Rails Guides also have documentation for changes involving each version jump which makes your job easier. For example: &lt;a href="https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#upgrading-from-rails-6-1-to-rails-7-0" rel="noopener noreferrer"&gt;here&lt;/a&gt; is a documentation guide for &lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-6-1-to-7-0.html" rel="noopener noreferrer"&gt;upgrading the app from Rails 6.1 to 7.0&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you also have to &lt;a href="https://www.fastruby.io/blog/ruby/upgrades/upgrade-ruby-from-2.7-to-3.0.html" rel="noopener noreferrer"&gt;upgrade Ruby&lt;/a&gt; along with Rails, then the ideal approach is to upgrade them individually. This way you can minimize the risk of bugs creeping in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ignoring Deprecation Warnings
&lt;/h2&gt;

&lt;p&gt;We have seen some clients silencing the deprecation warnings instead of addressing them. The reason deprecation warning exists is to alert developers of a behavior of the framework, ruby, or another gem that will not be supported in the future and to give them time to move away from the deprecated behavior which will eventually not be supported.&lt;/p&gt;

&lt;p&gt;The problem with silencing the warning is when the deprecated behavior is no longer supported, it will break the application and the developer will have to either roll back the upgrade or make changes to the app really quickly and no developer would like to be in that situation.&lt;/p&gt;

&lt;p&gt;Additionally, &lt;a href="https://www.fastruby.io/blog/rails/upgrades/deprecation-warnings-rails-guide.html" rel="noopener noreferrer"&gt;addressing the deprecation warnings&lt;/a&gt; are fairly straightforward most of the time, and the deprecation warning message also indicates how to fix / change the code to get rid of the warning.&lt;/p&gt;

&lt;p&gt;It is way easier to &lt;a href="https://www.fastruby.io/blog/rails/upgrades/active-record-5-1-api-changes.html" rel="noopener noreferrer"&gt;address a deprecation warning that is explicit&lt;/a&gt; (it tells you the code, the change and the line that’s triggering it) than fixing a bug that you don’t know where it’s happening and what is causing it.&lt;/p&gt;

&lt;p&gt;We’ve seen hours of work of debugging to end up finding that the bug was due to something that had a deprecation warning which was never addressed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading Gems
&lt;/h2&gt;

&lt;p&gt;When you are upgrading Rails, chances are that you are going to upgrade other gems along with it. But, how do you find out which gems need to be upgraded and to what versions? There are 2 ways to go about this.&lt;/p&gt;

&lt;p&gt;First, running &lt;code&gt;bundle update&lt;/code&gt; and then it will update every gem to the most compatible version. The problem with this approach is that since it is going to_update all the possible gems_ with a compatible higher version, you are exposing yourself to more bugs which can come due to code / behavior changes in the upgraded gems.&lt;/p&gt;

&lt;p&gt;Second, running &lt;code&gt;bundle update rails&lt;/code&gt; and then it will break with incompatibility messages across your gem set and with errors indicating which gems need to be updated. The downside to this approach is that it takes longer as you have to manually upgrade gems iteratively until the &lt;code&gt;bundle update&lt;/code&gt; is successful.&lt;/p&gt;

&lt;p&gt;The upsides are huge though as chances of bugs creeping comes down as you have only upgraded gems that absolutely needed to be upgraded.&lt;/p&gt;

&lt;p&gt;You can read more about it &lt;a href="https://www.fastruby.io/blog/ruby/dual-boot/generating-gemfile-next-lock.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Undocumented Monkey Patches and Forking gems
&lt;/h2&gt;

&lt;p&gt;Yes, Ruby is great because the powers it gives you and one of them is monkey patching existing behavior. This is great because it solves your problems in the short term.&lt;/p&gt;

&lt;p&gt;But we have seen enough projects where the monkey patched behavior is left with no comments explaining why it was done in the first place and the developer who did this is no more working at the organization to explain and now you are left with no option but to make sense of the patch work all by yourself which is very close to guess work.&lt;/p&gt;

&lt;p&gt;Another big problem we have encountered over the years is forking gems. Lots of times, developers fork open source gems and make changes to it to fit their needs.&lt;/p&gt;

&lt;p&gt;But those changes are never submitted to the open source project and the reason for the fork is left to yourself without documenting it. Now it is the team who is working on the upgrade that is left to make sense of why the fork was done, and is it safe to go the released rubygems version of it or make the fork work with the upgraded Rails and other gems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lack of test coverage or testing strategy
&lt;/h2&gt;

&lt;p&gt;To ensure that the upgrade of the Rails app is successfully released on production, you need either of the 2 things at least.&lt;/p&gt;

&lt;p&gt;First, a good test coverage of your application. Anything above 80% test coverage is considered good if it includes coverage of the critical paths of the applications.&lt;/p&gt;

&lt;p&gt;Second, awareness of all the critical paths of the application and ability to test them manually.&lt;/p&gt;

&lt;p&gt;If you do not have either of the 2 things in place, it increases the chances of encountering bugs upon releasing it on production. Even with both the strategies in place, you can still set yourself for a buggy release on production and that can be due to not having a dedicated QA team or QA role defined in your workflow.&lt;/p&gt;

&lt;p&gt;When the developer doing the code review on the pull request ends up doing the QA of it as well it is not an ideal situation. It can lead to bugs not caught before they are released to your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not using the right tools
&lt;/h2&gt;

&lt;p&gt;There are many ways to go about upgrading your application, but choosing the right tools and the right strategy can go a long way in making your experience smooth.&lt;/p&gt;

&lt;p&gt;Something as trivial as branching and code arrangement strategy holds a lot of importance in your workflow. We have seen a lot of teams struggle or end up doing repetitive work because of lack of clearly defined strategy.&lt;/p&gt;

&lt;p&gt;To avoid confusion on how to run the upgrades, we contribute and maintain the &lt;a href="https://github.com/fastruby/next_rails" rel="noopener noreferrer"&gt;next_rails&lt;/a&gt; gem which:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Helps us in running 2 different versions of Ruby or Rails in the same branch&lt;/li&gt;
&lt;li&gt;Facilitates running specs for the 2 different versions of Ruby or Rails from the same branch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This goes a long way in debugging issues while doing upgrades without switching branches. It also helps in implementing a gradual deployment strategy with Kubernetes.&lt;/p&gt;

&lt;p&gt;We have also seen customers trying to do everything “the Rails way” when it does not make sense for them.&lt;/p&gt;

&lt;p&gt;For example, Active Storage is a great gem and very useful for someone allowing users to upload files. But one of our customers wanted to use Active Storage even though their use case did not fit to use it.&lt;/p&gt;

&lt;p&gt;What they already had made more sense for them. Using Active Storage would have made their systems more complex. So it is also important to know when to move away from the defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Having a Rollback Plan
&lt;/h2&gt;

&lt;p&gt;Finally, not having a rollback plan is a common mistake made while upgrading Ruby or Rails.&lt;/p&gt;

&lt;p&gt;Despite careful planning, unexpected issues can arise during the deployment. It is important to have a rollback plan in case of any issues that may arise during the deployment process.&lt;/p&gt;

&lt;p&gt;A rollback plan should include steps to revert to the previous version of Rails, restore db backups, and fix issues that caused the rollback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Upgrading your Ruby or Rails version is essential for keeping your application secure and up to date.&lt;/p&gt;

&lt;p&gt;However, upgrading can be a challenging task, especially when you make common mistakes.&lt;/p&gt;

&lt;p&gt;In this article, we discussed 7 common mistakes that developers make when upgrading Ruby or Rails and how to avoid them. By avoiding these mistakes, you can make the upgrade process smoother and less stressful.&lt;/p&gt;

&lt;p&gt;In conclusion, upgrading Rails can be a daunting task, but with the right approach, you can make it a seamless process.&lt;/p&gt;

&lt;p&gt;Happy upgrading!&lt;/p&gt;

&lt;p&gt;PS: Need help upgrading Ruby or Rails to their latest stable version? &lt;a href="https://www.fastruby.io" rel="noopener noreferrer"&gt;We have some availability to help your team upgrade Ruby/Rails&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>rails</category>
      <category>upgrades</category>
    </item>
    <item>
      <title>How to Upgrade Rails Without a Test Suite</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Wed, 13 Jan 2021 09:00:00 +0000</pubDate>
      <link>https://dev.to/fastruby/how-to-upgrade-rails-without-a-test-suite-403h</link>
      <guid>https://dev.to/fastruby/how-to-upgrade-rails-without-a-test-suite-403h</guid>
      <description>&lt;p&gt;Sometimes you will be caught between a rock and a hard place. For example: You really need to upgrade your Rails application because Heroku is discontinuing your stack in two weeks so you don't have the time to write an entire test suite.&lt;/p&gt;

&lt;p&gt;Sometimes the situation will be that your boss really needs you to upgrade Ruby or Rails to use a version that gets security updates and they won't allow you to write tests beforehand.&lt;/p&gt;

&lt;p&gt;This article will explain how to ship a Rails upgrade project without a test suite. If your application has no tests, you will learn how to upgrade without having to write tests first.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up Staging&lt;/li&gt;
&lt;li&gt;Check the Changelog&lt;/li&gt;
&lt;li&gt;Write a Testing Script&lt;/li&gt;
&lt;li&gt;Set up Error Tracking&lt;/li&gt;
&lt;li&gt;Set up Dual Booting&lt;/li&gt;
&lt;li&gt;Upgrade Development and Test&lt;/li&gt;
&lt;li&gt;Deploy to Staging and Test&lt;/li&gt;
&lt;li&gt;Deploy to Production&lt;/li&gt;
&lt;li&gt;If you have the time, write integration tests&lt;/li&gt;
&lt;li&gt;Summary&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Set up Staging
&lt;/h2&gt;

&lt;p&gt;Have a staging server that is identical to your production server. You will need to thoroughly test your changes in a staging environment before you deploy any changes to production.&lt;/p&gt;

&lt;p&gt;Strive to make this environment as similar to production as possible. Fill it up with production-like data, so that you can test real world scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Changelog
&lt;/h2&gt;

&lt;p&gt;Many of the changes in the new Rails version won't affect your application. Read the most notable changes in the changelog and manually find whether the APIs that changed are getting used in your application's code.&lt;/p&gt;

&lt;p&gt;In this step there will be a lot of &lt;em&gt;grepping&lt;/em&gt; or finding in files within your code editor. If you want to learn how to do this, we wrote an article about that: &lt;a href="https://www.fastruby.io/blog/rails/upgrades/our-rails-upgrade-process.html" rel="noopener noreferrer"&gt;How we upgrade Rails applications&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Write a Testing Script
&lt;/h2&gt;

&lt;p&gt;If you know the application from top to bottom, write down several manual test scenarios. If you don't, work with the product owner to define a set of steps that you can run to smoke test the application in your development, staging, and production environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Set up Error Tracking
&lt;/h2&gt;

&lt;p&gt;If you don't have this configured, make sure that you set it up before you deploy anything to staging or production.&lt;/p&gt;

&lt;p&gt;There are many options when it comes to error tracking. Some of the most popular alternatives are &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;; &lt;a href="https://www.honeybadger.io" rel="noopener noreferrer"&gt;Honeybadger&lt;/a&gt;; or &lt;a href="https://rollbar.com" rel="noopener noreferrer"&gt;Rollbar&lt;/a&gt;. This will come in handy when you deploy your code and you get real users using your application on the new version of Rails.&lt;/p&gt;

&lt;p&gt;Considering your application does not have a test suite, I can guarantee that you will get exception notifications once your code is live. You want to make sure that you can get all the necessary information so that you can quickly patch those bugs.&lt;/p&gt;

&lt;p&gt;Even applications with a solid test suite and great code coverage will find exceptions in production. So this is a great idea even if you are not deploying a major Rails upgrade. :)&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Set up Dual Booting
&lt;/h2&gt;

&lt;p&gt;This will help you with the next steps across all your environments. Using this technique you will save a lot of time, because you will be able to run your application with Rails X or Y by changing a single environment variable.&lt;/p&gt;

&lt;p&gt;We have explained how to dual boot in this article:&lt;a href="https://www.fastruby.io/blog/upgrade-rails/dual-boot/dual-boot-with-rails-6-0-beta.html" rel="noopener noreferrer"&gt;How to Dual Boot Ruby on Rails&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Upgrade Development and Test
&lt;/h2&gt;

&lt;p&gt;If you have set up dual booting correctly, you will be able to run your current Rails version like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bundle exec rails server

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your new Rails version like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;next bundle exec rails server

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can start smoke testing your application in your development environment. This step will include a lot of steps that look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read and execute your testing script
&lt;/li&gt;
&lt;li&gt;Monitor your logs and error tracking service
&lt;/li&gt;
&lt;li&gt;Find issues&lt;/li&gt;
&lt;li&gt;Patch them&lt;/li&gt;
&lt;li&gt;And repeat...&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you feel comfortable that you have tested your critical user paths and everything looks good, you can move on to deploying your changes to your staging environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Deploy to Staging and Test
&lt;/h2&gt;

&lt;p&gt;Now you are at a point where you can involve your QA team. Once you have deployed and tested your changes in staging, you can involve more people in helping you manually test critical user flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Deploy to Production and Test
&lt;/h2&gt;

&lt;p&gt;There are two safe-ish ways to deploy your Rails upgrade to production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gradual deployment&lt;/li&gt;
&lt;li&gt;Full deployment&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Gradual deployment
&lt;/h3&gt;

&lt;p&gt;This is another step that benefits from your dual booting code. You can deploy your changes to only a few application instances in your cluster. Then you will have your cluster running both versions in parallel (Rails X and Y).&lt;/p&gt;

&lt;p&gt;You can set up your production environment to direct only 10% of the traffic to the new version of your code. This article explains how you can do that:&lt;a href="https://engineering.adwerx.com/continuously-integrating-a-major-rails-upgrade-53a0d2e12505" rel="noopener noreferrer"&gt;Continuously Integrating a Major Rails Upgrade&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Full deployment
&lt;/h3&gt;

&lt;p&gt;If your application does not get a lot of traffic at night, you might want to deploy your changes to all your visitors then. This approach is certainly riskier because slower traffic means fewer users which means fewer "testers" for real world scenarios.&lt;/p&gt;

&lt;p&gt;In both scenarios, you will eventually sit and wait for errors. Depending on the severity of the errors, you might want to roll back your changes and go back tostep 6.&lt;/p&gt;

&lt;p&gt;If there are only a few minor bugs, you might want to hot fix them in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Integration Tests
&lt;/h2&gt;

&lt;p&gt;If you have the time, I would highly recommend writing some integration tests. You can use RSpec and Capybara to write some simple tests to view them in a browser.&lt;/p&gt;

&lt;p&gt;You don't have to get too creative when writing these tests, you can basically write something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# spec/features/homepage_spec.rb
require "rails_helper"

RSpec.describe "Homepage at FastRuby.io", js: true do
  it "does not blow up" do
    visit "/"

    expect(page).to have_text("Need to upgrade Rails but don't have the time?")
  end
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can write basic, smoke-testy integration tests that mimic the behavior of end users and get to a non-zero amount of code coverage in a few minutes. If you have the time, doing this will save you time in your workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary (TL;DR)
&lt;/h2&gt;

&lt;p&gt;Someone is going to test your application. It could be you, writing automated tests and increasing code coverage. It might be your QA team, testing things work as expected in staging and production. In the worst case, it will be your end users trying to do something in production.&lt;/p&gt;

&lt;p&gt;If you don't have a test suite, it does not mean that you cannot upgrade a dependency of your application. It usually means that it will take longer than if you had a well-written test suite, because there will be a lot of manual testing during the project.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>How to Prepare Your Rails App for an Upgrade</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Tue, 29 Dec 2020 09:00:00 +0000</pubDate>
      <link>https://dev.to/fastruby/how-to-prepare-your-rails-app-for-an-upgrade-1am0</link>
      <guid>https://dev.to/fastruby/how-to-prepare-your-rails-app-for-an-upgrade-1am0</guid>
      <description>&lt;p&gt;&lt;em&gt;This article is part of our Rails Upgrade series. To see more of them, &lt;a href="https://fastruby.io/blog/tags/upgrades" rel="noopener noreferrer"&gt;click here&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This article will cover the most important aspects that you need to know to prepare your &lt;a href="http://rubyonrails.org/" rel="noopener noreferrer"&gt;Ruby on Rails&lt;/a&gt; application for working on an upgrade.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Test Coverage&lt;/li&gt;
&lt;li&gt;Staging and Production&lt;/li&gt;
&lt;li&gt;Patch Version&lt;/li&gt;
&lt;li&gt;Incompatibilities&lt;/li&gt;
&lt;li&gt;Dual Boot&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  1. Code Coverage
&lt;/h3&gt;

&lt;p&gt;Not all applications are good candidates for a Rails upgrade project. We strongly advise against upgrading big applications which are running in production with minimal test coverage.&lt;/p&gt;

&lt;p&gt;Make sure that you have at least 80% test coverage before starting the upgrade project. If you don't have a solid test suite (or a dedicated QA team), you will likely find many problems which will force you to roll back the upgrades as soon as they hit production.&lt;/p&gt;

&lt;p&gt;You can easily calculate your coverage by using &lt;a href="https://github.com/colszowka/simplecov" rel="noopener noreferrer"&gt;simplecov&lt;/a&gt;. Here is a &lt;a href="https://www.fastruby.io/blog/rails/upgrades/simplecov/rails-upgrades-simplecov.html" rel="noopener noreferrer"&gt;tutorial&lt;/a&gt; about how to use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Staging and Production
&lt;/h3&gt;

&lt;p&gt;We advise all of our clients to follow a &lt;a href="https://nvie.com/posts/a-successful-git-branching-model/" rel="noopener noreferrer"&gt;Git flow&lt;/a&gt; workflow and to actively manage at least two environments: staging and production.&lt;/p&gt;

&lt;p&gt;Every change should be code reviewed using pull requests and it should run the entire test suite. Once all checks have passed, changes should be deployed to staging. After your QA team has tested your critical flow in staging, you can deploy the changes to production.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Patch Version
&lt;/h3&gt;

&lt;p&gt;Before working on an upgrade you should make sure that you are running the application with the latest patch version. That way, you make sure that the latest security patches have been installed. On top of that, with more recent versions, you will get a series of deprecation warnings that will be useful in finding out what needs to be done.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Incompatibilities
&lt;/h3&gt;

&lt;p&gt;Have you ever gotten tangled in dependencies? You need to upgrade dependency A, but that's incompatible with Rails 5.0 because dependency B (which is a dependency of A) is not compatible with that version of ActiveRecord).&lt;/p&gt;

&lt;p&gt;Before you start going down the rabbit hole, make sure to check your &lt;code&gt;Gemfile.lock&lt;/code&gt; for incompatibilities. For that, you can use this website: &lt;a href="https://railsbump.org/" rel="noopener noreferrer"&gt;https://railsbump.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That will give you an idea of how many dependencies are not compatible with Rails 4, 5, or 6.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Dual Boot
&lt;/h3&gt;

&lt;p&gt;To help you switch between your current Rails version and the new one, you can create a dual boot mechanism. The fastest way is to install the handy gem &lt;a href="https://github.com/fastruby/next_rails" rel="noopener noreferrer"&gt;next_rails&lt;/a&gt;. You can initialize it by doing&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ gem install next_rails
$ next --init

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then set up two Rails versions in your Gemfile like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if next?
  gem 'rails', '~&amp;gt; 6.0.0'
else
  gem 'rails', '~&amp;gt; 5.2.3'
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes dependencies are backwards compatible with the current version of Rails. Within the libraries, you will find code that looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if Rails::VERSION &amp;gt;= 5.1
  # does X
else
  # does Y
end

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that is the case, then you might be able to just upgrade the dependency using &lt;code&gt;bundle update&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;Now that you have laid the groundwork for you upgrade, it is time to start the work itself. For details on what you specific version upgrade will require, check out the corresponding guide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-to-rails-3.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 2.3 to 3.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-to-rails-3-1.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 3.0 to 3.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-to-rails-3-2.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 3.1 to 3.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-3-2-to-4-0.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 3.2 to 4.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-4-0-to-4-1.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 4.0 to 4.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-4-1-to-4-2.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 4.1 to 4.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-4-2-to-5-0.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 4.2 to 5.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-5-0-to-5-1.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 5.0 to 5.1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-5-1-to-5-2.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 5.1 to 5.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-5-2-to-6-0.html" rel="noopener noreferrer"&gt;How to upgrade from Rails 5.2 to 6.0&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Churn vs. Complexity vs. Code Coverage</title>
      <dc:creator>Ernesto Tagwerker (he/him)</dc:creator>
      <pubDate>Wed, 13 May 2020 02:25:00 +0000</pubDate>
      <link>https://dev.to/fastruby/churn-vs-complexity-vs-code-coverage-38fm</link>
      <guid>https://dev.to/fastruby/churn-vs-complexity-vs-code-coverage-38fm</guid>
      <description>&lt;p&gt;Churn vs. Complexity analysis is a great way to find insights about the maintainability of a project. Two of my favorite authors have written great articles about the Churn vs. Complexity graph:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.agileconnection.com/article/getting-empirical-about-refactoring"&gt;Getting Empirical about Refactoring&lt;/a&gt; by Michael Feathers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.sandimetz.com/blog/2017/9/13/breaking-up-the-behemoth"&gt;Breaking up the Beheamoth&lt;/a&gt; by Sandi Metz&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This two-dimensional graph can be very useful in finding the files that are the hardest to maintain in your application. In this article I will explain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How you can calculate these metrics and use them in your legacy project&lt;/li&gt;
&lt;li&gt;How code coverage metrics can guide your technical debt's collection efforts&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Churn
&lt;/h2&gt;

&lt;p&gt;Churn by itself is a useful metric. It will tell you which files are the ones that are constantly changing. Change takes time, so files that are constantly changing are costing you time and money.&lt;/p&gt;

&lt;p&gt;It will make you wonder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why are these files changing so much?&lt;/li&gt;
&lt;li&gt;Are requirements constantly changing?&lt;/li&gt;
&lt;li&gt;Are we truly understanding business rules correctly?&lt;/li&gt;
&lt;li&gt;Is our application poorly designed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your legacy application is written in Ruby, you can use a gem called &lt;a href="https://github.com/danmayer/churn"&gt;&lt;code&gt;churn&lt;/code&gt;&lt;/a&gt; to find the files that are constantly changing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calculating Churn in Ruby
&lt;/h3&gt;

&lt;p&gt;You can install &lt;a href="https://rubygems.org/gems/churn"&gt;&lt;code&gt;churn&lt;/code&gt;&lt;/a&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem install churn

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then call it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;churn -e rb --start_date "10 years ago" lib -i spec

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This will show you a list of files sorted by their churn count (number of times that the file has changed in the SCM that you are using)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**********************************************************************
* Project Churn
**********************************************************************

Files
+-----------------------------------------------------------------------------------+---------------+
| file_path | times_changed |
+-----------------------------------------------------------------------------------+---------------+
| lib/metric_fu.rb | 50 |
| lib/metric_fu/version.rb | 44 |
| lib/metric_fu/configuration.rb | 36 |
| lib/metric_fu/reporting/templates/awesome/awesome_template.rb | 29 |
| lib/metric_fu/io.rb | 23 |
| lib/metric_fu/formatter/html.rb | 23 |
| lib/base/configuration.rb | 23 |
| lib/metric_fu/run.rb | 22 |
| lib/metric_fu/metrics/saikuro/saikuro.rb | 20 |
| lib/metric_fu/metric.rb | 20 |
+-----------------------------------------------------------------------------------+---------------+

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;By itself, this metric provides a limited insight:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These are the files that have changed the most since the beginning of the project&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It doesn't provide insights into why these files are changing as much as they do. If you open these files in your editor, you might find that files with high churn are super complex or super simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complexity
&lt;/h2&gt;

&lt;p&gt;As you go about making your changes, you will introduce complexity. There is noway around it. The best way to have no complexity is to have no code.&lt;/p&gt;

&lt;p&gt;It doesn't matter whether you are a rockstar object-oriented architect or a novice programmer. You will introduce complexity into a project.&lt;/p&gt;

&lt;p&gt;"Complexity" should not be considered a negative term. "Unnecessary complexity" might be considered an anti-pattern. Good object-oriented programming calls for &lt;a href="https://wiki.c2.com/?CouplingAndCohesion"&gt;loose coupling and high cohesion&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You want modules that follow &lt;a href="http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod"&gt;SOLID&lt;/a&gt; principles. When you notice your module is becoming extremely complex, you will want to refactor it into two or more modules. These modules will send and receive messages between each other, which will increase coupling (as loosely as possible) and hopefully improve cohesion per module.&lt;/p&gt;

&lt;p&gt;As you go about submitting pull request to ship new features, change existing ones, or patch bugs, the maintainability of your project will vary according to the modules you introduce or change.&lt;/p&gt;

&lt;p&gt;The bigger the project, the more complexity, the harder it will be to maintain it, because there will be more moving pieces and loosely coupled modules.&lt;/p&gt;

&lt;p&gt;If you refactor a huge module, into two smaller modules, you will notice that &lt;a href="https://docs.codeclimate.com/docs/cognitive-complexity"&gt;cognitive complexity&lt;/a&gt; per module will (hopefully) get better. You won't have one huge, complex module anymore, you will have two or more modules that are easier to understand.&lt;/p&gt;

&lt;p&gt;Modules that have one and only one responsibility will be easier to maintain and test. Future changes will be easier because you will be able to quickly understand what the module is doing.&lt;/p&gt;

&lt;p&gt;In the next section I will explain how you can calculate complexity for all your Ruby modules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calculating Complexity in Ruby
&lt;/h3&gt;

&lt;p&gt;There are many tools out there (e.g. &lt;a href="https://www.fastruby.io/blog/ruby/quality/code-quality-ruby-gems.html"&gt;three awesome libraries to assess code quality&lt;/a&gt;) that can help you find the most complex files in your project. One of the most popular tools in the Ruby world is &lt;a href="https://github.com/seattlerb/flog"&gt;&lt;code&gt;flog&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/julianrubisch/attractor"&gt;Attractor&lt;/a&gt; uses &lt;code&gt;flog&lt;/code&gt; to calculate complexity for each module. I really like the report that it generates. You can see all the modules in one page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4_w1II9s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://fastruby.io/blog/assets/images/attractor.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4_w1II9s--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://fastruby.io/blog/assets/images/attractor.png" alt="Attractor Report for MetricFu Ruby gem"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By itself this metric can take you in the wrong direction. Sometimes you will find extremely complex files which never change. So they are likely not your priority when it's time to refactor code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Churn vs. Complexity
&lt;/h2&gt;

&lt;p&gt;In his article, Michael Feathers, states the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nXaKyBa---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://fastruby.io/blog/assets/images/churn-vs-complexity-graph-by-michael-feathers.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nXaKyBa---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://fastruby.io/blog/assets/images/churn-vs-complexity-graph-by-michael-feathers.jpg" alt="Churn vs Complexity by Michael Feathers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These diagrams give us quite a bit of information. The upper right quadrant is particularly important. These files have a high degree of complexity, and they change quite frequently. [...] These classes are particularly ripe for arefactoring investment&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I agree that this graph is great for finding &lt;em&gt;refactoring candidates&lt;/em&gt;. The problem is that sometimes these candidates have no tests to verify their behavior. So sometimes we don't have time to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write tests&lt;/li&gt;
&lt;li&gt;Refactor complex files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes we have to pick one or the other. So in my opinion it would be great to include another metric into our analysis: Code Coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Coverage
&lt;/h2&gt;

&lt;p&gt;When we start maintaining a module, we don't usually just look at its churn and complexity. We also look at the tests that describe its expected behavior.&lt;/p&gt;

&lt;p&gt;Usually a module that has a decent test suite (basic code coverage) will be easier to understand than one without tests. We will have one more resource to learn about a module's behavior.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code coverage metrics don't tell whether the test suite is good or bad. It tells you how many statements of your application are exercised by your test suite. You should not blindly trust code coverage percentages without doing a quick code review of the tests that are present.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't you just love it when a module has great tests that describe the expected behavior of its public API?&lt;/p&gt;

&lt;h2&gt;
  
  
  Churn vs. Complexity vs. Code Coverage
&lt;/h2&gt;

&lt;p&gt;When I look at all the modules in the project, I want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many times has this file changed since the beginning of the project? (Churn)&lt;/li&gt;
&lt;li&gt;How complex is this module? (Complexity)&lt;/li&gt;
&lt;li&gt;What's the code coverage associated with this module? (Code Coverage)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This new dimension will tell us which files are the most changed, the most complicated, and the least covered with tests.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Refactoring modules that lack proper tests can quickly turn into a nightmare. You don't want your refactoring efforts to blow up in your face when changes hit production. And you probably don't want to test all your changes manually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By adding a third dimension to the churn vs complexity graph, you will be able to gather new insights about your modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should I refactor this module?&lt;/li&gt;
&lt;li&gt;Should I increase test coverage before I refactor this module?&lt;/li&gt;
&lt;li&gt;Do I have time to do both this week?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Calculating a Churn vs. Complexity vs. Code Coverage Graph in Ruby
&lt;/h3&gt;

&lt;p&gt;Last year I published a library that can help you take all these metrics into account: &lt;a href="https://github.com/fastruby/skunk"&gt;Skunk&lt;/a&gt;. It relies on &lt;a href="https://github.com/whitesmith/rubycritic"&gt;RubyCritic&lt;/a&gt; and &lt;a href="https://github.com/colszowka/simplecov"&gt;SimpleCov&lt;/a&gt; to get all the metrics it needs.&lt;/p&gt;

&lt;p&gt;You can install it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem install skunk

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And run it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skunk lib/

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;It won't generate a graph (yet) but it will generate a table with all the modules in the &lt;code&gt;lib&lt;/code&gt; directory. It will sort files from worst to best (in terms of technical debt):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-----------------------------------------------------+----------------------------+----------------------------+----------------------------+----------------------------+----------------------------+
| file | skunk_score | churn_times_cost | churn | cost | coverage |
+-----------------------------------------------------+----------------------------+----------------------------+----------------------------+----------------------------+----------------------------+
| lib/skunk/cli/commands/default.rb | 166.44 | 1.6643999999999999 | 3 | 0.5548 | 0 |
| lib/skunk/cli/application.rb | 139.2 | 1.392 | 3 | 0.46399999999999997 | 0 |
| lib/skunk/cli/command_factory.rb | 97.6 | 0.976 | 2 | 0.488 | 0 |
| test/test_helper.rb | 75.2 | 0.752 | 2 | 0.376 | 0 |
| lib/skunk/rubycritic/analysed_module.rb | 48.12 | 1.7184 | 2 | 0.8592 | 72.72727272727273 |
| test/lib/skunk/cli/commands/status_reporter_test.rb | 45.6 | 0.456 | 1 | 0.456 | 0 |
| lib/skunk/cli/commands/base.rb | 29.52 | 0.2952 | 3 | 0.0984 | 0 |
| lib/skunk/cli/commands/status_reporter.rb | 8.0 | 7.9956 | 3 | 2.6652 | 100.0 |
| test/lib/skunk/rubycritic/analysed_module_test.rb | 2.63 | 2.6312 | 2 | 1.3156 | 100.0 |
+-----------------------------------------------------+----------------------------+----------------------------+----------------------------+----------------------------+----------------------------+

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://github.com/fastruby/skunk#what-is-the-skunkscore"&gt;SkunkScore&lt;/a&gt; is a function of complexity and code coverage.&lt;/p&gt;

&lt;p&gt;Let's say that your application has two extremely complex modules: &lt;code&gt;user&lt;/code&gt; and &lt;code&gt;product&lt;/code&gt;. Let's assume that both of them have changed a lot (churn count at 100) and are extremely complex (1,000 flog points).&lt;/p&gt;

&lt;p&gt;In this example, if &lt;code&gt;user&lt;/code&gt; has more tests than &lt;code&gt;product&lt;/code&gt;, &lt;code&gt;user&lt;/code&gt;'s SkunkScore will be lower than &lt;code&gt;product&lt;/code&gt;'s. Next week you could start refactoring &lt;code&gt;user&lt;/code&gt; or increasing code coverage for &lt;code&gt;product&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;By looking at this table you will be able to quickly decide about what to do: You might want to write tests for a complex module, refactor a complex module that is well covered by integration tests, or both!&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Coverage as a Prioritization Mechanism
&lt;/h2&gt;

&lt;p&gt;In big projects you might notice hundreds of modules in the upper right quadrant of the Churn vs. Complexity graph. You might ask yourself: What do I do first? Do I write a test for module User? Do I refactor module Product?&lt;/p&gt;

&lt;p&gt;I strongly believe that Code Coverage is a great signal for complementing churn and complexity metrics. I hope you can use it in your next legacy project to gradually pay off technical debt!&lt;/p&gt;

</description>
      <category>churn</category>
      <category>complexity</category>
      <category>codequality</category>
    </item>
  </channel>
</rss>
