Jekyll2024-03-12T14:57:18-04:00https://fastruby.io/blog/rss.xmlThe Rails Tech Debt BlogFastRuby.io | Rails Upgrade ServiceOmbuLabsHow Do You Know When Your App is Not Compliant?2024-03-12T14:53:58-04:002024-03-12T14:53:58-04:00https://fastruby.io/blog/how-do-you-know-if-app-is-noncompliant<p>Ensuring that your company’s website is current with compliance standards is extremely important and essential for any Rails application. Operating with a compliant application guarantees security that can help with handling sensitive data and maintaining users’ trust. The more compliant your website is, the more secure it will be against data breaches, which helps users feel safe when they’re using it.</p>
<p>So what does it take to be compliant? In this article, we will focus on security and cover some indicators to help identify if your Rails app might not be compliant anymore.</p>
<!--more-->
<h3 id="understanding-compliance-requirements">Understanding Compliance Requirements</h3>
<p>Compliance requirements relevant to Rails applications include but are not limited to common standards and regulations such as GDPR, PCI DSS, HIPAA, and accessibility guidelines like WCAG. This can involve various aspects such as security, privacy, accessibility, and legal requirements.</p>
<h3 id="security-vulnerabilities">Security Vulnerabilities</h3>
<p>Security is an essential component for compliance. If your Rails app is not regularly updated with the latest security patches or if vulnerabilities are discovered in the libraries or dependencies it uses, it could become non-compliant with security standards.</p>
<p>Rails itself is really good at prioritizing security and provides many built-in security features. However as an application continues to evolve and grow with the introduction of new features, third party libraries or dependencies, chances are that vulnerabilities could have been introduced.</p>
<p>Regularly updating dependencies, implementing secure coding practices, and conducting thorough security assessments are essential steps to mitigate vulnerabilities and comply with industry standards like OWASP (Open Web Application Security Project) guidelines. This can save a company from any possible data breaches.</p>
<p>Resources listed below are helpful in identifying what type of security risks can exist and how to handle them:</p>
<ul>
<li><a href="https://guides.rubyonrails.org/security.html">Rails Guides on Securing applications</a></li>
<li><a href="https://owasp.org/www-project-top-ten/">Top ten security risks to be aware of</a></li>
<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html">Rails Security Tips Cheatsheet</a></li>
</ul>
<h3 id="legal-and-regulatory-requirements">Legal and Regulatory Requirements</h3>
<p>Depending on the nature of the app and the targeted industry, compliance with specific legal and regulatory requirements may be necessary. This could include regulations such as HIPAA or FINRA. If your application has not been audited more frequently, there is a chance that some of the rules and regulations for compliance with these standards have changed. This could indicate that the app is not compliant and this needs to be addressed immediately.</p>
<h3 id="data-privacy">Data Privacy</h3>
<p>If your app handles sensitive data (such as personal information or payment details) and fails to comply with data privacy regulations like GDPR, CCPA or PCI DDS, it could result in non-compliance. This includes inadequate data encryption, improper handling of user consent, or insufficient data access controls. As your application and user base expands, the risk of mishandling user data and sensitive information will increase as well. Periodic assessments and audits can help identify potential vulnerabilities before they escalate.</p>
<h3 id="accessibility">Accessibility</h3>
<p>Compliance with accessibility standards such as the Web Content Accessibility Guidelines (WCAG) ensures that the app is usable by individuals with disabilities. Signs of accessibility issues include inaccessible forms, missing alternative text for images, and lack of keyboard navigation support. Automated accessibility testing tools and manual testing with assistive technologies can help identify and fix these issues.</p>
<h3 id="conclusion">Conclusion</h3>
<p>Identifying non-compliance in your Rails app is crucial for maintaining data security, protecting user privacy, and upholding legal and regulatory requirements. By understanding common signs of non-compliance and implementing proactive measures to address these issues, you can ensure that your Rails app remains compliant and trustworthy for users.</p>
<p>Need help keeping your Rails applications secure? <a href="https://www.fastruby.io/security-audit">Contact us for a security audit!</a></p>aisayoEnsuring that your company’s website is current with compliance standards is extremely important and essential for any Rails application. Operating with a compliant application guarantees security that can help with handling sensitive data and maintaining users’ trust. The more compliant your website is, the more secure it will be against data breaches, which helps users feel safe when they’re using it. So what does it take to be compliant? In this article, we will focus on security and cover some indicators to help identify if your Rails app might not be compliant anymore.Ruby & Sinatra Compatibility Table2024-03-08T10:14:03-05:002024-03-08T10:14:03-05:00https://fastruby.io/blog/sinatra-and-ruby-compatability-table<p><a href="https://sinatrarb.com/">Sinatra</a> is known in the Ruby world for being a lightweight framework for building Ruby web applications with minimal effort.</p>
<p>Over time Sinatra has been through many versions, and sometimes it gets complicated keeping track of which versions of Sinatra are compatible with which versions of Ruby. Therefor, we made a handy chart!</p>
<!--more-->
<p>If you find yourself or your company struggling on an upgrade for either Sinatra or Ruby let us know, our company specializes in upgrades! <a href="/our-services">We have all different packages, for all different size companies</a>.</p>
<table class="ruby-compatibility-table">
<thead>
<tr>
<td><a href="https://rubygems.org/gems/sinatra" target="_blank">Sinatra Version</a></td>
<td><a href="https://www.ruby-lang.org/en/downloads/releases/" target="_blank">Required Ruby Version</a></td>
<td>Recommended Ruby Version</td>
</tr>
</thead>
<tbody>
<tr>
<td>4.0</td>
<td>>= 2.7.8</td>
<td>3.3.Z</td>
</tr>
<tr>
<td>3.2.0</td>
<td>>= 2.6.0</td>
<td>3.3.Z</td>
</tr>
<tr>
<td>3.1.0</td>
<td>>= 2.6.0</td>
<td>3.2.Z</td>
</tr>
<tr>
<td>3.0.X</td>
<td>>= 2.6.0</td>
<td>3.1.Z</td>
</tr>
<tr>
<td>2.2.X</td>
<td>>= 2.3.0</td>
<td>3.0.Z</td>
</tr>
<tr>
<td>2.1.0</td>
<td>>= 2.3.0</td>
<td>2.7.1</td>
</tr>
<tr>
<td>2.0.X</td>
<td>>= 2.2.0</td>
<td>2.4.1</td>
</tr>
<tr>
<td>1.4.x</td>
<td>>= 1.8.7</td>
<td>2.0.0</td>
</tr>
</tbody>
</table>
<p><br />
To find more information about the most recent Ruby releases check out this
page: <a href="https://www.ruby-lang.org/en/downloads/releases/">Ruby Releases</a></p>
<h2 id="feedback-wanted-updates">Feedback Wanted: Updates</h2>
<p>Note that this table goes to Sinatra 1.4.X, before that we were not able to find information on specific Ruby versions, but let us know if you have find incompatibilities for earlier versions.</p>
<p>Feel free to get in contact with us through social media, on <a href="https://ruby.social/@FastRuby">mastadon</a> or <a href="https://twitter.com/fastrubyio">Twitter</a>.</p>
<p>We will continue to update this article as new versions of Sinatra are released.</p>fionadlSinatra is known in the Ruby world for being a lightweight framework for building Ruby web applications with minimal effort. Over time Sinatra has been through many versions, and sometimes it gets complicated keeping track of which versions of Sinatra are compatible with which versions of Ruby. Therefor, we made a handy chart!The Evolution of ActiveModel::Error in the Rails Framework2024-03-05T11:23:46-05:002024-03-05T11:23:46-05:00https://fastruby.io/blog/the-evolution-of-activemodel-error-in-rails-framework<p>Rails progression emphasizes simplicity and productivity. Through versions, Rails integrated tools, enhanced performance, and adapted to industry standards, keeping a focus on developer happiness and efficiency. <code class="highlighter-rouge">ActiveModel::Error</code> is an example of that. On this blog post, we’ll dive into the evolution of this object.</p>
<!--more-->
<p>If you find yourself trying to upgrade your Rails application, it’s very likely that you will have to deal with changes that were made to <code class="highlighter-rouge">ActiveModel::Error</code> over time.</p>
<p>There are some key differences between versions 5.0 and 6.0 of Rails on how it deals with validation errors in the model layer.</p>
<p>Back in Rails 3.0 <code class="highlighter-rouge">ActiveModel::Error</code> was inheriting from the <code class="highlighter-rouge">ActiveSupport::OrderedHash</code> class, and it responded to <code class="highlighter-rouge">Hash</code> methods. It already included methods like <code class="highlighter-rouge">full_messages</code>, <code class="highlighter-rouge">details</code>, getters and setters.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Rails 3.0 - activemodel/lib/active_model/errors.rb</span>
<span class="k">class</span> <span class="nc">Errors</span> <span class="o"><</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">OrderedHash</span>
</code></pre></div></div>
<p>This <code class="highlighter-rouge">ActiveSupport::OrderedHash</code> inheritance was changed to an initializer later on Rails 3.1 to instantiate the error’s messages attribute.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Rails 3.1 - activemodel/lib/active_model/errors.rb</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">base</span><span class="p">)</span>
<span class="vi">@base</span> <span class="o">=</span> <span class="n">base</span>
<span class="vi">@messages</span> <span class="o">=</span> <span class="no">ActiveSupport</span><span class="o">::</span><span class="no">OrderedHash</span><span class="p">.</span><span class="nf">new</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Rails 4.0 changed the <code class="highlighter-rouge">messages</code> attribute initialization from <code class="highlighter-rouge">ActiveSupport::OrderedHash</code> to use the <code class="highlighter-rouge">Hash</code> class, changing the behavior of the <code class="highlighter-rouge">to_xml</code>, <code class="highlighter-rouge">as_json</code>, and <code class="highlighter-rouge">to_hash</code> methods. This change was made to adjust to Ruby version 1.9 or higher as Rails 4 had a minimum requirement of Ruby 1.9++ by that time.
You can check <a href="https://github.com/rails/rails/pull/4930">the pull request here</a>.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Rails 4.0 - activemodel/lib/active_model/errors.rb</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">base</span><span class="p">)</span>
<span class="vi">@base</span> <span class="o">=</span> <span class="n">base</span>
<span class="vi">@messages</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Lately, in Rails 5.0 they included the <code class="highlighter-rouge">details</code> attributes in the initializer to determine what validator has failed. You can check the <a href="https://github.com/rails/rails/pull/18322">pull request here</a>.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">base</span><span class="p">)</span>
<span class="vi">@base</span> <span class="o">=</span> <span class="n">base</span>
<span class="vi">@messages</span> <span class="o">=</span> <span class="p">{}</span>
<span class="vi">@details</span> <span class="o">=</span> <span class="no">Hash</span><span class="p">.</span><span class="nf">new</span> <span class="p">{</span> <span class="o">|</span><span class="n">details</span><span class="p">,</span> <span class="n">attribute</span><span class="o">|</span> <span class="n">details</span><span class="p">[</span><span class="n">attribute</span><span class="p">]</span> <span class="o">=</span> <span class="p">[]</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Once <a href="https://github.com/rails/rails/commit/6ec8ba16d85d5feaccb993c9756c1edcbbf0ba13">this commit</a> got in the Rails repository, they started deprecating <code class="highlighter-rouge">getters</code>, <code class="highlighter-rouge">setters</code> and <code class="highlighter-rouge">[]=</code> methods because of inconsistent behavior when dealing with then:</p>
<blockquote>
<p><code class="highlighter-rouge">errors.messages[:key]</code> and <code class="highlighter-rouge">errors.get(:key)</code> can be accessed only by symbol, but <code class="highlighter-rouge">errors["key"]</code> can be access by both string or symbol</p>
</blockquote>
<blockquote>
<p><code class="highlighter-rouge">errors.set(:key, ["error"])</code> is overwriting all errors, but <code class="highlighter-rouge">errors[:key] = "error"</code> (which should do the same thing) pushes error to existing ones.</p>
</blockquote>
<blockquote>
<p><code class="highlighter-rouge">errors.set("key", ["error"])</code> will not convert key to symbol, but <code class="highlighter-rouge">errors["key"] = "error"</code> and <code class="highlighter-rouge">errors.add("key", :invalid)</code> will convert it.</p>
</blockquote>
<p>You can check the discussion <a href="https://github.com/rails/rails/pull/18631">in this pull request</a>.</p>
<p>Rails 5.0 also deprecated the <code class="highlighter-rouge">add_on_empty</code> and <code class="highlighter-rouge">add_on_blank</code> methods in a following <a href="https://github.com/rails/rails/commit/259d33db8cae4f139c4646077f1637a8224dfdb2">commit</a>.</p>
<p>Rails 6.0 brings new methods to <code class="highlighter-rouge">ActiveModel::Error</code>:</p>
<blockquote>
<p><code class="highlighter-rouge">slice</code>, that was already provided to <code class="highlighter-rouge">Hash</code> via <code class="highlighter-rouge">ActiveSupport</code></p>
</blockquote>
<blockquote>
<p><code class="highlighter-rouge">of_kind?</code> that allow us to check presence of a specific error</p>
</blockquote>
<blockquote>
<p>and adds a new configuration option to customize format of the <code class="highlighter-rouge">full_message</code> output</p>
</blockquote>
<p>Rails 6.1 changes <code class="highlighter-rouge">ActiveModel::Error</code> to improve its object orientation behavior in <a href="https://github.com/rails/rails/pull/32313">this pull request</a>. The changes include a query interface, enable more precise testing, and access to error details.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">model</span><span class="p">.</span><span class="nf">errors</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="ss">:foo</span><span class="p">,</span> <span class="ss">bar: </span><span class="mi">3</span><span class="p">).</span><span class="nf">first</span>
</code></pre></div></div>
<p>Also making it easier to find the message with corresponding details of one particular error:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># model.errors.details</span>
<span class="p">{</span><span class="ss">:name</span><span class="o">=></span><span class="p">[{</span><span class="ss">error: :foo_error</span><span class="p">,</span> <span class="ss">count: </span><span class="mi">1</span><span class="p">},</span> <span class="p">{</span><span class="ss">error: :bar_error</span><span class="p">},</span> <span class="p">{</span><span class="ss">error: :foo_error</span><span class="p">,</span> <span class="ss">count: </span><span class="mi">3</span><span class="p">}]}</span>
</code></pre></div></div>
<p>This change also opened the possibility of advancing modding, as errors are now objects so we can add functionality on top of them. For example, we can create custom methods to disable global attribute prefixes on an error’s full messages.</p>
<p>Some methods like <code class="highlighter-rouge">as_json</code>, <code class="highlighter-rouge">add</code>, and <code class="highlighter-rouge">include?</code> remain unchanged after this Rails 6.1 change. Other methods were deprecated (<code class="highlighter-rouge">full_message</code>, <code class="highlighter-rouge">generate_message</code>, <code class="highlighter-rouge">has_key</code>) and new methods were added to this class: <code class="highlighter-rouge">messages_for</code>, <code class="highlighter-rouge">where</code>, and <code class="highlighter-rouge">import</code>.</p>
<p>The change tries its best at maintaining backward compatibility, however some edge cases won’t be covered. For example, <code class="highlighter-rouge">errors#first</code> will return <code class="highlighter-rouge">ActiveModel::Error</code> and manipulating <code class="highlighter-rouge">errors.messages</code> and <code class="highlighter-rouge">errors.details</code> hashes directly will have no effect.</p>
<p>Rails 7.0 removes all previous deprecated methods from <code class="highlighter-rouge">ActiveModel::Error</code> from the code.</p>
<h3 id="conclusion">Conclusion</h3>
<p>In conclusion, the <code class="highlighter-rouge">ActiveModel::Error</code> emerges as an essential component within the Rails environment, playing a crucial role in the robustness and adaptability of modern Rails applications. Its ability to seamlessly handle and manage validation errors not only enhances the user experience by providing clear and concise error messages but also contributes significantly to the overall maintainability of the code. By encapsulating error details within this specialized object, developers can streamline error handling processes, ensuring a more efficient and organized approach to troubleshooting and debugging.</p>
<p><code class="highlighter-rouge">ActiveModel::Error</code> stands as a testament to Rails’ commitment to simplicity, convention over configuration, and the creation of applications that are not only powerful but also resilient and developer-friendly.</p>
<p>Running an end-of-life Rails version in production and in need of an upgrade? <a href="https://www.fastruby.io/#contactus">Send us a message, we can help!</a></p>hmdrosRails progression emphasizes simplicity and productivity. Through versions, Rails integrated tools, enhanced performance, and adapted to industry standards, keeping a focus on developer happiness and efficiency. ActiveModel::Error is an example of that. On this blog post, we’ll dive into the evolution of this object.Dual-Boot Ruby2024-02-27T09:14:13-05:002024-02-27T09:14:13-05:00https://fastruby.io/blog/dual-boot-ruby<p>As we mentioned many times, at FastRuby.io we like to use the <a href="/blog/tags/dual-boot">Dual-Boot technique</a> during upgrades to quickly test the same code with the current and the next version of what we are upgrading. We usually talk about dual-booting Rails versions but this can be used to upgrade Ruby itself too. We have to make some changes to adapt the technique, and we’ll explain the basic changes in this article.</p>
<!--more-->
<h2 id="dual-boot-gems-vs-dual-boot-ruby">Dual-Boot Gems vs Dual-Boot Ruby</h2>
<p>When we talk about the Dual-Boot technique, the main concept is to have 2 different <code class="highlighter-rouge">Gemfile.lock</code> files, one with the dependencies for the current version of the app, and one with the dependencies for the next version.</p>
<p>We typically include a snippet like this at the top of the <code class="highlighter-rouge">Gemfile</code>:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">next?</span>
<span class="no">File</span><span class="p">.</span><span class="nf">basename</span><span class="p">(</span><span class="kp">__FILE__</span><span class="p">)</span> <span class="o">==</span> <span class="s2">"Gemfile.next"</span>
<span class="k">end</span>
</code></pre></div></div>
<p>And we use the <code class="highlighter-rouge">next?</code> helper method to set different versions of gems like this:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="k">next</span><span class="p">?</span>
<span class="n">gem</span> <span class="s2">"rails"</span><span class="p">,</span> <span class="ss">github: </span><span class="s2">"rails/rails"</span>
<span class="k">else</span>
<span class="n">gem</span> <span class="s2">"rails"</span><span class="p">,</span> <span class="s2">"~> 7.1.0"</span>
<span class="k">end</span>
</code></pre></div></div>
<p>Then, the application can be run with different commands to run one version or the other: <code class="highlighter-rouge">rails s</code> will run Rails 7.1, <code class="highlighter-rouge">BUNDLE_GEMFILE=Gemfile.next rails s</code> will run Rails’ main branch.</p>
<p>In order to dual-boot Ruby, we can use the same technique but with a few considerations.</p>
<h2 id="considerations">Considerations</h2>
<p>Versions of the gems are generally only defined in the <code class="highlighter-rouge">Gemfile</code> files, but the Ruby version is defined in many places. The most common are:</p>
<ul>
<li>.ruby-version file (used by some version managers)</li>
<li>.tool-versions file (used by some version managers)</li>
<li>Gemfile (used by bundler, RVM, Heroku, and other tools)</li>
<li>Gemfile.lock (the Ruby version in this file is mostly informative)</li>
<li>Dockerfile (used by Docker to prepare the container)</li>
<li>CI setup steps</li>
</ul>
<p>And applications can also have the expected Ruby version defined in other not-so-common places.</p>
<p>We have to make sure that a dual-boot setup is compatible with all of them to avoid a negative impact on the developer experience.</p>
<p>It’s important to note that, when dual-booting a gem, we only need to run the same command with different <code class="highlighter-rouge">BUNDLE_GEMFILE</code> env variables to quickly test the different version. However, when dual-booting Ruby, we need to first switch the version that is currently active using our Ruby version manager before running the app with the desired version.</p>
<h3 id="gemfile-files">Gemfile files</h3>
<p>It is really common to specify the required Ruby version in the <code class="highlighter-rouge">Gemfile</code> like this:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ruby</span> <span class="s2">"3.2.2"</span>
</code></pre></div></div>
<p>This is used by Bundler to ensure that we are using the expected Ruby version when running the app, but it’s also used by other tools like RVM or Heroku to pick which version of Ruby to set up.</p>
<p>To support the dual-boot, we can change that line to this:</p>
<div class="language-rb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ruby_version</span> <span class="o">=</span> <span class="k">next</span><span class="p">?</span> <span class="p">?</span> <span class="s2">"3.3.0"</span> <span class="p">:</span> <span class="s2">"3.2.2"</span>
<span class="n">ruby</span> <span class="n">ruby_version</span>
</code></pre></div></div>
<p>With this code, Heroku will install Ruby 3.2.2 when building the application, and at the same time it allows us to run the app with either Ruby 3.3.0 or 3.2.2 without failing.</p>
<blockquote>
<p>Note that the version displayed in the <code class="highlighter-rouge">Gemfile.lock</code> file is not a problem for us: the <code class="highlighter-rouge">Gemfile.lock</code> file will show the current Ruby version, and the <code class="highlighter-rouge">Gemfile.next.lock</code> file will show the next. Those files are not meant to be used with the other Ruby.</p>
</blockquote>
<h3 id="version-files">Version files</h3>
<p>Different Ruby version managers handle the <code class="highlighter-rouge">*version</code> files in different ways. Managers like <code class="highlighter-rouge">asdf</code> or <code class="highlighter-rouge">rbenv</code> will update their version files when switching rubies, so there’s nothing to change for these files to make them compatible with a dual-boot setup. It’s important to not commit changes in these files when switching between Ruby versions though. We want these files in the repository to always show the current Ruby version and not the next one.</p>
<h3 id="docker-configuration">Docker configuration</h3>
<p>If the setup includes using Docker to run the application, we have a complete article on <a href="todo:add-link-when-published">how to dual-boot both Ruby and Rails using docker</a>.</p>
<h3 id="ci-setup">CI setup</h3>
<p>Finally, we need to update the CI files to run the different jobs with both versions of Ruby. Most CI solutions support a <code class="highlighter-rouge">matrix</code> feature to define multiple values of a given tool and they will automatically execute one job for each of those values.</p>
<p>Here’s an example using a matrix to setup <a href="https://github.com/fastruby/next_rails/blob/main/.github/workflows/main.yml#L16">multiple Ruby versions in GitHub Actions</a>.</p>
<p>This is not enough though, since we also need to specify the <code class="highlighter-rouge">BUNDLE_GEMFILE</code> variable.</p>
<p>To do that we have multiple options:</p>
<p><strong>Duplicate jobs</strong></p>
<p>One option is to duplicate the original job and change the values to set up Ruby and the environment variables. You can re-use the original information using <a href="https://www.linode.com/docs/guides/yaml-anchors-aliases-overrides-extensions/">YAML anchors, aliases, and overrides</a> if the CI service supports them.</p>
<p><strong>Jobs Matrix</strong>
Another option is to use the matrix feature with extra configuration for each Ruby version if supported.</p>
<p>GitHub Actions, for example, supports matrix configurations using the <code class="highlighter-rouge">include</code> property to set other variables for a given matrix value, and the parameters will include the extra keys:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">strategy</span><span class="pi">:</span>
<span class="na">matrix</span><span class="pi">:</span>
<span class="na">ruby</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">3.2.2"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">3.3.0"</span><span class="pi">]</span>
<span class="na">include</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">gemfile</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Gemfile.lock"</span>
<span class="na">ruby</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.2.2"</span>
<span class="pi">-</span> <span class="na">gemfile</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Gemfile.next.lock"</span>
<span class="na">ruby</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.3.0"</span>
</code></pre></div></div>
<p>Check the documentation on <a href="https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#expanding-or-adding-matrix-configurations">Expanding or adding matrix configurations</a> for more information.</p>
<p>CircleCI supports a different pattern by defining all the possible values and excluding the combinations we don’t want to run:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">workflows</span><span class="pi">:</span>
<span class="na">workflow</span><span class="pi">:</span>
<span class="na">jobs</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">build</span><span class="pi">:</span>
<span class="na">matrix</span><span class="pi">:</span>
<span class="na">parameters</span><span class="pi">:</span>
<span class="na">ruby</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">3.2.2"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">3.3.0"</span><span class="pi">]</span>
<span class="na">gemfile</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">Gemfile.lock"</span><span class="pi">,</span> <span class="s2">"</span><span class="s">Gemfile.next.lock"</span><span class="pi">]</span>
<span class="na">exclude</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">ruby</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.2.2"</span>
<span class="na">gemfile</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Gemfile.next.lock"</span>
<span class="pi">-</span> <span class="na">ruby</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.3.0"</span>
<span class="na">gemfile</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Gemfile.lock"</span>
</code></pre></div></div>
<p>Check the documentation on <a href="https://circleci.com/docs/configuration-reference/#excluding-sets-of-parameters-from-a-matrix">Excluding sets of parameters from a matrix</a> for more information.</p>
<p>TravisCI offers a different pattern to define the different configurations explicitly instead of using implicit combinations:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">jobs</span><span class="pi">:</span>
<span class="na">include</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">rvm</span><span class="pi">:</span> <span class="s">3.2.2</span>
<span class="na">gemfile</span><span class="pi">:</span> <span class="s">Gemfile</span>
<span class="pi">-</span> <span class="na">rvm</span><span class="pi">:</span> <span class="s">3.3.0</span>
<span class="na">gemfile</span><span class="pi">:</span> <span class="s">Gemfile.next</span>
</code></pre></div></div>
<p>Check the documentation on <a href="https://docs.travis-ci.com/user/build-matrix/#listing-individual-jobs">Listing individual jobs</a> for more information.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Dual-booting Ruby is similar to any other dependency, but there are some important differences that we need to address to do it right. There are more ways to do this but we try to minimize the disruption to the normal development workflow as much as possible. With these ideas you can default to use the current Ruby version while also being able to use the next version on demand with a few commands.</p>
<p>It’s important to note that, in some cases, dual-booting Ruby is not needed when the upgrade is simple enough. An initial test using the new Ruby version can be done without adding the complexity of the dual-boot to make an informed decision.</p>
<p>Running behind and need to upgrade to Ruby 3.3.0? <a href="/#contact-us">Let us help you!</a></p>arieljuodAs we mentioned many times, at FastRuby.io we like to use the Dual-Boot technique during upgrades to quickly test the same code with the current and the next version of what we are upgrading. We usually talk about dual-booting Rails versions but this can be used to upgrade Ruby itself too. We have to make some changes to adapt the technique, and we’ll explain the basic changes in this article.The Rails Developer’s Reference to PostgreSQL indexes2024-02-21T07:16:36-05:002024-02-21T07:16:36-05:00https://fastruby.io/blog/the-rails-developers-reference-to-postgresql-indexes<p>In a <a href="/blog/common-problems-in-rails-performance">previous article</a>, we listed down common culprits that led to a sub-optimal performance in Rails applications. One of the culprits was missing or incorrect indexes.</p>
<p>Therefore we thought it would be very useful to have a handy reference to the different kinds of indexes, when you should use them and maybe even when <em>not</em> to use them.</p>
<!--more-->
<ul>
<li><a href="#different-kinds-of-indexes">Different kinds of indexes</a>
<ul>
<li><a href="#b-tree-indexes">B-Tree Indexes</a></li>
<li><a href="#hash-indexes">Hash Indexes</a></li>
<li><a href="#gist-and-sp-gist-indexes">GiST and SP-GiST Indexes</a></li>
<li><a href="#gin-indexes">GIN Indexes</a></li>
<li><a href="#brin-indexes">BRIN Indexes</a></li>
</ul>
</li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
<h2 id="different-kinds-of-indexes">Different kinds of indexes</h2>
<h3 id="b-tree-indexes">B-Tree Indexes</h3>
<p>The Binary Tree index, or B-Tree index, is the default index type we get from a <code class="highlighter-rouge">CREATE INDEX</code> statement.</p>
<p>It’s probably also the most common index type we’ll use as developers. The use case is straightforward. You should use a B-Tree index when:</p>
<ol>
<li>Your queries retrieve small datasets relative to the table size.</li>
<li>The column you want to index has high cardinality, which is to say, their data isn’t replicated too many times in different rows.</li>
</ol>
<blockquote>
<p>For example, if you have a products table that has a category column, it’s natural that many products will have the same value for their category. Therefore, category has <em>low cardinality</em>.
Inversely, if the data is unique for every row (say, the product id), that column has <em>high cardinality</em></p>
</blockquote>
<p>The unique id column in tables are usually the more obvious candidates for this index.</p>
<p>Generally speaking, PostgreSQL will use this index for most of the commonly used comparison operators (<code class="highlighter-rouge">></code>,<code class="highlighter-rouge">>=</code>, <code class="highlighter-rouge"><</code>, <code class="highlighter-rouge"><=</code>, <code class="highlighter-rouge">=</code>, <code class="highlighter-rouge">BETWEEN</code> and <code class="highlighter-rouge">IN</code>) as well as the null checking operators (<code class="highlighter-rouge">IS NULL</code> and <code class="highlighter-rouge">IS NOT NULL</code>). It can even use B-Tree indexes for pattern matching operators, <a href="https://www.postgresql.org/docs/16/indexes-types.html#INDEXES-TYPES-BTREE">under certain conditions</a>.</p>
<p>There are a few ways to add an index in an ActiveRecord migration:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># In a create_table block</span>
<span class="n">t</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="ss">:column_name</span><span class="p">)</span>
<span class="c1"># or outside of the block</span>
<span class="n">add_index</span><span class="p">(</span><span class="ss">:table_name</span><span class="p">,</span> <span class="ss">:column_name</span><span class="p">)</span>
</code></pre></div></div>
<p>See the docs for <a href="https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_index"><code class="highlighter-rouge">add_index</code></a> for all the possible options.</p>
<p>Finally, as a SQL statement:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">column_name_idx</span> <span class="k">ON</span> <span class="k">table_name</span><span class="p">(</span><span class="k">column_name</span><span class="p">);</span>
</code></pre></div></div>
<h3 id="hash-indexes">Hash Indexes</h3>
<p>For practical purposes, a hash index can be considered to have a specialized use case for columns that could also be indexed with a B-Tree but that we know for a fact it will always be queried using the <code class="highlighter-rouge">=</code> operator.</p>
<p>The way hash indexes work is that they create a 32-bit hash for the value in the indexed column. Which brings us to the first drawback for this type of index: they only work for equality comparisons. Another drawback to be considered is that they have a maintenance overhead during data modifications since the database must solve hash collisions by rehashing the data.</p>
<p>Even with all these drawbacks, they are faster for equality comparisons than B-Tree indexes.</p>
<p>The query planner will consider using hash indexes whenever the indexed column is involved in a comparison with the equal operator.</p>
<p>It’s worth noting that it is <em>very rare</em> that using a hash index will ever be needed. However, if your use case fits into its requirements and you need to get more performance than what you have with a B-Tree index, it’s worth testing some queries using a hash index and see if there is some real benefit.</p>
<p>To create a Hash index using ruby and ActiveRecord, pass the <code class="highlighter-rouge">using: 'hash'</code> option to <code class="highlighter-rouge">index</code> or <code class="highlighter-rouge">add_index</code>:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># In a create_table block</span>
<span class="n">t</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'hash'</span><span class="p">)</span>
<span class="c1"># or outside of the block</span>
<span class="n">add_index</span><span class="p">(</span><span class="ss">:table_name</span><span class="p">,</span> <span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'hash'</span><span class="p">)</span>
</code></pre></div></div>
<p>And as a SQL statement:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">column_name_idx</span> <span class="k">ON</span> <span class="k">table_name</span><span class="p">(</span><span class="k">column_name</span><span class="p">)</span> <span class="k">USING</span> <span class="n">HASH</span><span class="p">;</span>
</code></pre></div></div>
<h3 id="gist-and-sp-gist-indexes">GiST and SP-GiST Indexes</h3>
<p>GiST (Generalized Search Tree) and SP-GiST (Space Partitioned) indexes aren’t a specific kind of index but rather an infrastructure that allows database users to define indexing for complex data types.</p>
<p>Out of the box, PostgreSQL comes with implementations for several geometric shapes and text search. These data classes that implement the required GiST functions are called <em>operator classes</em></p>
<p>What this means, practically speaking, is that such indexes have a very diverse yet <em>specialized</em> set of applications and use cases, ranging from finding some point within a given distance from some coordinate to full text search.</p>
<p>Usage of these indexes is application dependent so it’s best to read PostgreSQL’s own documentation on the matter and determine if your use case might fit: <a href="https://www.postgresql.org/docs/16/gist.html">GiST chapter</a> and <a href="https://www.postgresql.org/docs/16/spgist.html">SP-GiST chapter</a></p>
<p>To create a GiST or SP-GiST index in Rails:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># In a create_table block</span>
<span class="n">t</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'gist'</span><span class="p">)</span>
<span class="n">t</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'spgist'</span><span class="p">)</span>
<span class="c1"># or outside of the block</span>
<span class="n">add_index</span><span class="p">(</span><span class="ss">:table_name</span><span class="p">,</span> <span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'gist'</span><span class="p">)</span>
<span class="n">add_index</span><span class="p">(</span><span class="ss">:table_name</span><span class="p">,</span> <span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'spgist'</span><span class="p">)</span>
</code></pre></div></div>
<p>And with SQL:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">column_name_idx</span> <span class="k">ON</span> <span class="k">table_name</span><span class="p">(</span><span class="k">column_name</span><span class="p">)</span> <span class="k">USING</span> <span class="n">GIST</span><span class="p">;</span>
<span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">column_name_idx</span> <span class="k">ON</span> <span class="k">table_name</span><span class="p">(</span><span class="k">column_name</span><span class="p">)</span> <span class="k">USING</span> <span class="n">SPGIST</span><span class="p">;</span>
</code></pre></div></div>
<h3 id="gin-indexes">GIN Indexes</h3>
<p>GIN stands for Generalized Inverted Index. These indexes are appropriate for data that contain multiple component values, such as arrays. An inverted index contains a separate entry for each component value and efficiently handles queries that test for these component values.</p>
<p>Hence the “Inverted” in the name. A normal B-Tree index will have one place to represent the data for one row, whereas a GIN index can have the same row referenced in multiple places. It’s analogous to the table of contents at the back of many books: the same term (or component value of a row) is referenced in multiple different pages.</p>
<p>Like GiST and SP-GiST indexes, the exact way how a GIN index maps a column of a given data type, depends on the GIN operator class.</p>
<p>It’s important to note that GIN indexes have one big downside: They’re expensive to update. Because it can reference the same row multiple times, any changes to that row means a change to all indexes referencing that row.</p>
<p>To create a GIN index in Rails:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># In a create_table block</span>
<span class="n">t</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'gin'</span><span class="p">)</span>
<span class="c1"># or outside of the block</span>
<span class="n">add_index</span><span class="p">(</span><span class="ss">:table_name</span><span class="p">,</span> <span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'gin'</span><span class="p">)</span>
</code></pre></div></div>
<p>And with SQL:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">INDEX</span> <span class="n">column_name_idx</span> <span class="k">ON</span> <span class="k">table_name</span><span class="p">(</span><span class="k">column_name</span><span class="p">)</span> <span class="k">USING</span> <span class="n">GIN</span><span class="p">;</span>
</code></pre></div></div>
<h3 id="brin-indexes">BRIN Indexes</h3>
<p>BRIN stands for Block Range INdexes.</p>
<p>BRIN indexes will divide your data into pages, according to their storage order in memory, and keep track of the beginning and ending values of the indexed columns in that page. They’ll do this for the entire table.</p>
<blockquote>
<p>As an example, if we index a <code class="highlighter-rouge">created_at</code> column, the index will store the value for <code class="highlighter-rouge">created_at</code> of the first row in a given page and the value for the same column for the last row of the same page, and so forth for all pages into which the table will be divided.</p>
</blockquote>
<p>Given this property, this index is only recommended when there is a strong correlation between the data stored and it’s physical order in memory. Building on the previous example, <code class="highlighter-rouge">created_at</code> columns are usually good candidates because rows added yesterday or earlier in the day come before the current row in memory, and subsequent insertions will also be inserted in memory closely following the value of <code class="highlighter-rouge">created_at</code>, even though there isn’t necessarily a logical rule establishing this behavior.</p>
<p>Also, since it breaks the table into pages, it’s important to know how much memory each row of your table occupies. See [this blog post by Janet Carson] on ways to do this. The reason this is important is that if your table is too small and the index stores too much data into a single page, the index won’t render the speed benefits we want for queries. Once you’ve estimated your row size, knowing a page of memory has 8192 bytes, you can estimate how big your block range should be and set the <code class="highlighter-rouge">pages_per_range</code> storage parameter accordingly.</p>
<p>Also, <em>BRIN indexes do not auto update by default</em>. Either the database administrator must explicitly call index maintenance functions or the index must be created with <code class="highlighter-rouge">autosummarize = 1</code>.</p>
<p>Finally, in order to know if your column’s logical ordering has a good direct correlation to the storage order in memory, the <code class="highlighter-rouge">pg_stats</code> table can give you this information via the <code class="highlighter-rouge">correlation</code> column. The closer to 1, the better the fit for a BRIN index.</p>
<p>To create this index in Rails:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># In a create_table block</span>
<span class="n">t</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'brin'</span><span class="p">)</span>
<span class="c1"># or outside of the block</span>
<span class="n">add_index</span><span class="p">(</span><span class="ss">:table_name</span><span class="p">,</span> <span class="ss">:column_name</span><span class="p">,</span> <span class="ss">using: </span><span class="s1">'brin'</span><span class="p">)</span>
</code></pre></div></div>
<p>I was unable to find a way to call these methods in a way that I could pass the <code class="highlighter-rouge">autosummarize</code> storage parameter. If anyone know, I’d be happy to hear and add it here. In the meantime, we can always execute raw SQL in a migration:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Anywhere inside the change method</span>
<span class="n">execute</span><span class="o"><<-</span><span class="no">SQL</span><span class="sh">
CREATE INDEX developer_created_at_idx ON developers USING brin (created_at) WITH (autosummarize=1);
</span><span class="no">SQL</span>
</code></pre></div></div>
<h3 id="conclusion">Conclusion</h3>
<p>Hopefully the discussion above can help any Rails developers out there understand the different indexes available in PostgreSQL and how to properly pick the one that will help them tune their application and make it run as smooth as possible.</p>
<p>In case you still need help optimizing your queries, understanding where to add indexes, what type of indexes to use or with other performance related issues, make sure to <a href="/tune">send us a message</a>. We can help!</p>mateuspereiraIn a previous article, we listed down common culprits that led to a sub-optimal performance in Rails applications. One of the culprits was missing or incorrect indexes. Therefore we thought it would be very useful to have a handy reference to the different kinds of indexes, when you should use them and maybe even when not to use them.Ruby & Roda Compatibility Table2024-02-20T10:08:29-05:002024-02-20T10:08:29-05:00https://fastruby.io/blog/ruby-roda-compatibility-table<p><a href="https://roda.jeremyevans.net/">Roda</a> is a web toolkit (or framework) that focusses on
simplicity, reliability, extensibility, and performance.
This is a short post to show the compatibility between Roda and <a href="https://www.ruby-lang.org/en/">Ruby</a>
across different versions.</p>
<!--more-->
<table class="ruby-compatibility-table">
<thead>
<tr>
<td><a href="https://rubygems.org/gems/roda/versions" target="_blank">Roda Version</a></td>
<td><a href="https://www.ruby-lang.org/en/downloads/releases/" target="_blank">Required Ruby Version</a></td>
<td>Recommended Ruby Version</td>
</tr>
</thead>
<tbody>
<tr>
<td>3.X.Z</td>
<td>>= 1.9.2</td>
<td>2.4.1 - 3.3.Z</td>
</tr>
<tr>
<td>2.X.Z (>= 2.10.0)</td>
<td>>= 1.8.7</td>
<td>2.2.3 - 2.4.1</td>
</tr>
<tr>
<td>>= 0.9.0 (<= 2.9.0)</td>
<td>>= 0</td>
<td>2.1.2 - 2.2.3</td>
</tr>
</tbody>
</table>
<p><br /></p>
<p>To find more information about the most recent Ruby releases check out this
page: <a href="https://www.ruby-lang.org/en/downloads/releases/">Ruby Releases</a></p>
<h2 id="need-to-upgrade-ruby-or-roda">Need to Upgrade Ruby or Roda?</h2>
<p>Roda does not have upgrade guides per se, but the repo does provide an extensive <a href="https://github.com/jeremyevans/roda/blob/master/CHANGELOG">changelog</a>
that will guide you from one Roda version to the next.</p>
<p>If you don’t have the time to do it yourself you can hire our team to do it for you,
send us a message over here: <a href="/#contactus">Contact FastRuby.io | Ruby and Rails Upgrade Service</a></p>
<h2 id="feedback-wanted-updates">Feedback Wanted: Updates</h2>
<p>If you find that this article has fallen out of date, feel free to make a
comment for us to bring it up to speed. We will continue to update this article
as new versions of Ruby and Roda are released.</p>fbuysRoda is a web toolkit (or framework) that focusses on simplicity, reliability, extensibility, and performance. This is a short post to show the compatibility between Roda and Ruby across different versions.Cracking the Case on Flaky Tests: Tips for Build Confidence and Seamless Upgrades2024-02-09T07:00:43-05:002024-02-09T07:00:43-05:00https://fastruby.io/blog/cracking-the-code-on-flaky-specs<p>How many times have you or someone on your team brushed off a failing build with a casual, ‘It’s fine, it’s just a flaky spec—ignore it’?</p>
<p>If you’re nodding in agreement, you’re not alone. It’s a scenario familiar to many of us, especially when dealing with sprawling monolithic projects and untouched code sections.</p>
<!--more-->
<p>However, this attitude towards flaky tests can take a serious turn during upgrades. Upgrades rely on the capabilities of tests; if we can’t trust them, then uncertainty lingers.</p>
<p>Are they genuinely flaky, or have we unintentionally introduced issues during the upgrade? Has upgrading somehow made the tests flakier? We have run into both of these issues on past upgrades.</p>
<p>In this article we’ll give some tips on how to untangle the mystery behind flaky specs, with the aim of guiding you towards consistently passing builds. Ensuring your tests don’t have flakiness can help every upgrade become more likely to succeed.</p>
<h2 id="the-unpredictable-nature-of-flaky-tests">The Unpredictable Nature of Flaky Tests</h2>
<p>While flaky tests are often associated with integration tests using technologies like JS, Capybara-Webkit, or Selenium, other reasons are also possible. Oftentimes tests pass locally without issue, but fail CI over and over.</p>
<h3 id="common-causes-of-flaky-tests">Common Causes of Flaky Tests:</h3>
<p>In our experience of 100+ upgrade projects, we’ve come across flaky specs in all different types of projects. This section describes some of the most common causes of flaky specs.</p>
<h3 id="race-conditions">Race Conditions</h3>
<p>Flaky tests may arise from race conditions where the timing of execution influences the test outcome.</p>
<h3 id="leaked-state">Leaked State</h3>
<p>State leakage between tests can lead to unpredictable failures. Shared state should be prevented to minimize the impact of leaked state on the reliability of your test suite.</p>
<h3 id="networkthird-party-dependency">Network/Third-Party Dependency</h3>
<p>External dependencies, such as network calls or third-party services, introduce an element of unpredictability. <a href="/blog/test-doubles-testing-at-the-boundaries-of-your-ruby-application.html">Mocking and stubbing</a> should always be used to create a controlled test environment.</p>
<h3 id="randomness">Randomness</h3>
<p>Ironically, randomness itself can be a cause of flaky tests. It is usually a more steady option to choose specific values when testing instead of grabbing random ones, another possibility to control the randomness. For example if you want values for the age of a person, you could say random between 0 and 99.</p>
<h3 id="fixed-time-dependency">Fixed Time Dependency:</h3>
<p>Tests relying on fixed time values might be sensitive to time of day or system variations.</p>
<h2 id="analyzing-flaky-tests-asking-the-right-questions">Analyzing Flaky Tests: Asking the Right Questions</h2>
<p>Unraveling the mystery behind flaky tests often involves asking the right questions to pinpoint potential causes. By examining patterns and considering different aspects of your test environment, you can narrow down the root of the issue.</p>
<p>Here are some key questions to get you started on your investigation:</p>
<h3 id="timing-patterns">Timing Patterns:</h3>
<p>Is there any pattern to tests failing at specific times of day?</p>
<p>This could indicate a fixed time dependency issue, where the outcome of the test is influenced by the time it runs. Identifying such patterns helps uncover time-related vulnerabilities in your test suite.</p>
<p>One way to check this would be to write a script that would run the tests every few hours and log the output. This could help in identifying if there is a time pattern to the flaky tests. In our <a href="/blog/rspec/debug/how-to-debug-non-deterministic-specs.html">post about debugging non-deterministic specs</a> we go into more detail on this topic.</p>
<h3 id="test-order-sensitivity">Test Order Sensitivity:</h3>
<p>Do certain tests consistently fail when executed in a specific order?
Test order sensitivity may reveal issues with dependencies between tests. Investigating this can provide insights into shared state problems or race conditions affecting the stability of your test suite.</p>
<p>A good way to investigate this is the run the tests using a specific seed number. You can grab the seed number after running the tests once, and then use it to run them in the same order again.</p>
<p>This can also be really useful if something is failing in CI to see if it’s also failing locally.</p>
<p>For minitest:
<code class="highlighter-rouge">rails test TESTOPTS="--seed 1234"</code></p>
<p>or</p>
<p><code class="highlighter-rouge">rails test --seed 1234</code></p>
<p>For rspec:
<code class="highlighter-rouge">rspec --seed 1234</code></p>
<h3 id="external-dependencies-and-network-connections">External Dependencies and Network Connections:</h3>
<p>Are your tests interacting with external services or APIs?</p>
<p>Do flaky tests coincide with periods of network instability?</p>
<p>External dependencies often introduce variability in test outcomes. Understanding the impact of external factors on your tests is crucial for creating a more controlled and reliable testing environment. It’s <a href="/blog/testing/javascript/mocking-js-requests.html">important to use mocking and stubbing</a> instead of true API calls.</p>
<h3 id="randomness-and-seeds">Randomness and Seeds:</h3>
<p>Have you ensured proper seeding for tests involving randomness?</p>
<p>Flaky behavior can arise if randomness is not adequately controlled. Confirming the use of seeds for random processes ensures test reproducibility and minimizes unexpected variations.</p>
<h3 id="code-changes">Code Changes:</h3>
<p>Have recent code changes introduced new dependencies or altered test behavior?</p>
<p>Regularly reviewing code changes, especially those in proximity to failing tests, helps identify potential causes. Try to figure out when the test started failing, look back through build logs to see if something specific introduced the flakiness.</p>
<p>By systematically addressing these questions, you can gain valuable insights into the nature of flaky tests and take targeted actions to enhance the stability of your test suite.</p>
<p>Each question can serve as an investigative tool, bringing you one step closer to consistently green builds which will help make upgrades easier.</p>
<h2 id="some-more-tips-for-keeping-everything-up-to-date">Some More Tips for Keeping Everything up to Date.</h2>
<p>Asking the right questions to investigate your code base is not enough on it’s own. Team collaboration, monitoring, and staying on top of best practices all play a crucial roll in keeping the flaky specs at bay.</p>
<h3 id="test-maintenance-strategies">Test Maintenance Strategies:</h3>
<p>Discuss the importance of ongoing test maintenance to prevent flakiness over time. Encourage your team to regularly review and update tests, especially after significant code changes or upgrades.</p>
<h3 id="monitoring-and-alerting">Monitoring and Alerting:</h3>
<p>Suggest implementing monitoring and alerting systems that notify teams when a test becomes flaky. Early detection allows for timely investigation and resolution, minimizing the impact on build confidence.</p>
<h3 id="documentation-and-best-practices">Documentation and Best Practices:</h3>
<p>Emphasize the significance of well-documented tests. Clear documentation can help developers understand the purpose of each test, making it easier to identify potential issues and troubleshoot failures.</p>
<p>Don’t make tests DRY simply for the sake of making them DRY. Think about decisions around drying up tests, and if it will make it more difficult for your team to figure out where problems are arising in the test suite.</p>
<h3 id="collaboration-and-communication">Collaboration and Communication:</h3>
<p>Highlight the importance of collaboration between developers and QA teams. Encourage open communication channels to promptly address flaky tests, share insights, and collectively work towards maintaining a reliable test suite.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In the world of software development, the reliability of your test suite is so important. Flaky tests, though common, should not be dismissed.</p>
<p>By implementing the strategies outlined in this article—addressing common causes, analyzing test patterns, and fostering a proactive testing culture—you can pave the way to consistently green builds and seamless upgrades.</p>
<p>The journey to build confidence is an ongoing process that requires collaboration, and a commitment to the quality of your codebase.</p>
<p>Take these tips, integrate them into your development workflow, and let your test suite become a rock-solid foundation for your software projects. Happy testing!</p>
<p>If your team is planning to do an upgrade soon tests are so important for success. We can help you understand how to get ready for an upgrade with our <a href="/roadmap">Roadmap to Upgrade Rails</a>.</p>fionadlHow many times have you or someone on your team brushed off a failing build with a casual, ‘It’s fine, it’s just a flaky spec—ignore it’? If you’re nodding in agreement, you’re not alone. It’s a scenario familiar to many of us, especially when dealing with sprawling monolithic projects and untouched code sections.Dual-Boot Ruby on Rails using Docker2024-02-07T01:00:00-05:002024-02-07T01:00:00-05:00https://fastruby.io/blog/dual-boot-and-docker<p>Starting in Rails 7.1, <a href="https://guides.rubyonrails.org/7_1_release_notes.html#generate-dockerfiles-for-new-rails-applications">Docker files are added by default</a> in new applications, but <a href="https://www.docker.com">Docker</a> has been popular for Rails development for many years before that. At FastRuby.io, we use the <a href="/blog/tags/dual-boot">Dual-Boot technique</a> when we work on upgrades, and using that approach when an application uses Docker requires some extra steps to keep a great development experience.</p>
<!--more-->
<h2 id="initial-setup">Initial Setup</h2>
<p>We’ll use this initial basic setup as an example of the technique. Since applications can have Docker configured in completely different ways, this is more of a guide with the main elements that must be changed and should be adapted as needed.</p>
<h3 id="dockerfile">Dockerfile</h3>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> ruby:3.1</span>
<span class="k">WORKDIR</span><span class="s"> /code</span>
<span class="k">COPY</span><span class="s"> Gemfile ./</span>
<span class="k">COPY</span><span class="s"> Gemfile.lock ./</span>
<span class="k">RUN </span>bundle <span class="nb">install</span>
</code></pre></div></div>
<p>This is the minimal setup to tell Docker to create a Ruby 3.1 container, to copy our <code class="highlighter-rouge">Gemfile</code> and <code class="highlighter-rouge">Gemfile.lock</code> files, and to install the gems during the build.</p>
<h3 id="docker-composeyml">docker-compose.yml</h3>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.7"</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">web</span><span class="pi">:</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">command</span><span class="pi">:</span> <span class="s2">"</span><span class="s">rails</span><span class="nv"> </span><span class="s">s"</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">.:/code:delegated"</span>
</code></pre></div></div>
<p>This is the minimal setup to tell <a href="https://docs.docker.com/compose/">Docker Compose</a> what to do with our Dockerfile.</p>
<p>Then, all we need to do to start the Rails application is run <code class="highlighter-rouge">docker-compose up</code>.</p>
<blockquote>
<p>Throughout the article, I’ll use <code class="highlighter-rouge">docker-compose up</code>, but depending on your Docker installation you may need to use <code class="highlighter-rouge">docker compose up</code> instead (without the <code class="highlighter-rouge">-</code>).</p>
</blockquote>
<h2 id="dual-boot-ruby">Dual-Boot Ruby</h2>
<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># We define a RUBY_VERSION argument with a default value of the</span>
<span class="c"># Ruby version used before adding dual boot</span>
<span class="k">ARG</span><span class="s"> RUBY_VERSION=3.1</span>
<span class="c"># We use the RUBY_VERSION argument which will have the value defined</span>
<span class="c"># in the docker-compose.yml file, or the 3.1 fallback value</span>
<span class="k">FROM</span><span class="s"> ruby:${RUBY_VERSION}</span>
<span class="k">WORKDIR</span><span class="s"> /code</span>
<span class="k">COPY</span><span class="s"> Gemfile ./</span>
<span class="k">COPY</span><span class="s"> Gemfile.lock ./</span>
<span class="k">RUN </span>bundle <span class="nb">install</span>
</code></pre></div></div>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.7"</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">web</span><span class="pi">:</span> <span class="nl">&common</span>
<span class="na">build</span><span class="pi">:</span> <span class="nl">&build</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">args</span><span class="pi">:</span>
<span class="na">RUBY_VERSION</span><span class="pi">:</span> <span class="m">3.1</span>
<span class="na">command</span><span class="pi">:</span> <span class="s2">"</span><span class="s">rails</span><span class="nv"> </span><span class="s">s"</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">.:/code:delegated"</span>
<span class="na">web-next</span><span class="pi">:</span>
<span class="na"><<</span><span class="pi">:</span> <span class="nv">*common</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na"><<</span><span class="pi">:</span> <span class="nv">*build</span>
<span class="na">args</span><span class="pi">:</span>
<span class="na">RUBY_VERSION</span><span class="pi">:</span> <span class="m">3.2</span>
<span class="na">profiles</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">next</span><span class="pi">]</span>
</code></pre></div></div>
<p>We reuse the configuration from the original service to not need to duplicate everything using the <code class="highlighter-rouge">&common</code> and <code class="highlighter-rouge">&build</code> YAML anchors and respective aliases.</p>
<p>We use the <code class="highlighter-rouge">profiles: [next]</code> <a href="https://docs.docker.com/compose/compose-file/15-profiles/#profiles">property</a> in the <code class="highlighter-rouge">web-next</code> service so it is ignored when running <code class="highlighter-rouge">docker-compose up</code>. This avoids starting 2 apps at the same time trying to use the same port.</p>
<blockquote>
<p>If you want to run both versions of the application with <code class="highlighter-rouge">docker-compose up</code>, you can remove this <code class="highlighter-rouge">profiles: [next]</code> line and define different ports for each.</p>
</blockquote>
<p>Now we can run <code class="highlighter-rouge">docker-compose up</code> to run the Rails application using Ruby 3.1, and <code class="highlighter-rouge">docker-compose up web-next</code> to run it using Ruby 3.2.</p>
<h3 id="gemfile-consideration">Gemfile Consideration</h3>
<p>In most Rails applications, the <code class="highlighter-rouge">Gemfile</code> includes a <code class="highlighter-rouge">ruby x.y.z</code> declaration to specify the expected Ruby version to use. We can remove that declaration to avoid conflicts with the next Ruby version not matching the one defined in the <code class="highlighter-rouge">Gemfile</code>. In those cases we can add this step in the Dockerfile before running <code class="highlighter-rouge">bundle install</code>:</p>
<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Remove the `ruby x.y.z` declaration from the Gemfile file</span>
<span class="k">RUN </span><span class="nb">sed</span> <span class="nt">-i</span> <span class="s1">'s/^ruby.*$//g'</span> ./<span class="k">${</span><span class="nv">BUNDLE_GEMFILE</span><span class="k">}</span>
</code></pre></div></div>
<h2 id="dual-boot-rails">Dual-Boot Rails</h2>
<p>The first step is to set up the <code class="highlighter-rouge">Gemfile.next</code> file so we can bundle and run the application with different Rails versions on demand. This article shows <a href="https://www.fastruby.io/blog/upgrade-rails/dual-boot/dual-boot-with-rails-6-0-beta.html">how to dual boot</a> a Rails app.</p>
<p>Once the <code class="highlighter-rouge">Gemfile.next</code> and <code class="highlighter-rouge">Gemfile.next.lock</code> are ready, we can proceed with the changes to Docker.</p>
<div class="language-Dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> ruby:3.1</span>
<span class="k">WORKDIR</span><span class="s"> /code</span>
<span class="c"># We define a BUNDLE_GEMFILE argument with a default value of Gemfile</span>
<span class="k">ARG</span><span class="s"> BUNDLE_GEMFILE=Gemfile</span>
<span class="c"># We use the BUNDLE_GEMFILE argument which will have the value defined</span>
<span class="c"># in the docker-compose.yml file, with Gemfile as a fallback</span>
<span class="c"># Since Gemfile.next is a symlink to Gemfile, we must copy Gemfile but</span>
<span class="c"># with the Gemfile.next name so we have a file and not the symlink.</span>
<span class="k">COPY</span><span class="s"> Gemfile ./${BUNDLE_GEMFILE}</span>
<span class="k">COPY</span><span class="s"> ${BUNDLE_GEMFILE}.lock ./</span>
<span class="k">RUN </span><span class="nv">BUNDLE_GEMFILE</span><span class="o">=</span><span class="k">${</span><span class="nv">BUNDLE_GEMFILE</span><span class="k">}</span> bundle <span class="nb">install</span>
</code></pre></div></div>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.7"</span>
<span class="na">services</span><span class="pi">:</span>
<span class="na">web</span><span class="pi">:</span> <span class="nl">&common</span>
<span class="na">build</span><span class="pi">:</span> <span class="nl">&build</span>
<span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
<span class="na">args</span><span class="pi">:</span>
<span class="na">BUNDLE_GEMFILE</span><span class="pi">:</span> <span class="s">Gemfile</span>
<span class="na">command</span><span class="pi">:</span> <span class="s2">"</span><span class="s">rails</span><span class="nv"> </span><span class="s">s"</span>
<span class="na">volumes</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">.:/code:delegated"</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">BUNDLE_GEMFILE=Gemfile</span>
<span class="na">web-next</span><span class="pi">:</span>
<span class="na"><<</span><span class="pi">:</span> <span class="nv">*common</span>
<span class="na">build</span><span class="pi">:</span>
<span class="na"><<</span><span class="pi">:</span> <span class="nv">*build</span>
<span class="na">args</span><span class="pi">:</span>
<span class="na">BUNDLE_GEMFILE</span><span class="pi">:</span> <span class="s">Gemfile.next</span>
<span class="na">environment</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">BUNDLE_GEMFILE=Gemfile.next</span>
<span class="na">profiles</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">next</span><span class="pi">]</span>
</code></pre></div></div>
<p>When dual booting a gem, we need to add <code class="highlighter-rouge">Gemfile.next</code> both as an argument for the build process, and as an environment variable to use inside the container.</p>
<p>Now we can run <code class="highlighter-rouge">docker-compose up</code> to run the application using the current Rails version, and <code class="highlighter-rouge">docker-compose up web-next</code> to run it using the next Rails version.</p>
<h2 id="alternative-method-to-dual-boot-rails">Alternative Method to Dual-Boot Rails</h2>
<p>Instead of using different containers, an alternative method is to use a single container and bundle all the gems from both versions:</p>
<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> ruby:3.1</span>
<span class="k">WORKDIR</span><span class="s"> /code</span>
<span class="k">COPY</span><span class="s"> Gemfile ./</span>
<span class="k">COPY</span><span class="s"> Gemfile.lock ./</span>
<span class="k">RUN </span>bundle <span class="nb">install</span>
<span class="k">COPY</span><span class="s"> Gemfile.next ./</span>
<span class="k">COPY</span><span class="s"> Gemfile.next.lock ./</span>
<span class="k">RUN </span><span class="nv">BUNDLE_GEMFILE</span><span class="o">=</span>Gemfile.next bundle <span class="nb">install</span>
</code></pre></div></div>
<p>This has some pros and cons compared to the previous method.</p>
<h3 id="pros">Pros</h3>
<ul>
<li>We can open a <code class="highlighter-rouge">bash</code> session inside the container and run commands with the different Rails versions without having to leave the container.</li>
<li>We save some space by not having a second Docker image.</li>
<li>We only need to build one image, and we can change the <code class="highlighter-rouge">docker-compose.yml</code> file to run the container with a different <code class="highlighter-rouge">BUNDLE_GEMFILE</code> env variable as needed.</li>
</ul>
<h3 id="cons">Cons</h3>
<ul>
<li>If we use this Docker image for production, our deployment will be slower, bundling a second set of gems that we usually won’t use.</li>
<li>If we want to do something different for the next version (like using different environment variables or running different dependent services), the setup would be more complicated.</li>
<li>If we want to run the same test in the 2 Rails apps and debug them in parallel, we can’t do this in a single container.</li>
<li>This approach is not easy to use to dual boot Ruby, so if the upgrade process requires upgrading multiple versions of Ruby and Rails over time, this would be more inconsistent for the developer experience.</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>In this article we explained 2 approaches to apply the Dual-Boot technique when using Docker, with pros and cons for you to try them out and pick what works better for your needs. Both approaches have their benefits and you can also switch between them as you need for different tasks.</p>
<p>You can also combine them: install all gems of both Rails versions in a single container (instead of using build arguments) and use different services to run the container with one version of Rails or the other.</p>
<p>It’s important to note that these are not the only possible approaches. Docker is a really flexible tool with many ways to configure and run containers.</p>
<p>Do you need help with an upgrade? Do you use Docker in development or production? <a href="/#contact-us">We can help!</a></p>arieljuodStarting in Rails 7.1, Docker files are added by default in new applications, but Docker has been popular for Rails development for many years before that. At FastRuby.io, we use the Dual-Boot technique when we work on upgrades, and using that approach when an application uses Docker requires some extra steps to keep a great development experience.Largest Contentful Paint2024-02-05T04:54:26-05:002024-02-05T04:54:26-05:00https://fastruby.io/blog/lcp<p>Is your goal to rank first on Google? Have you already tried using the best keywords and strategies to rank higher but none of that has worked? It might be because your LCP, or Largest Contentful Paint, score is high and needs improvement.</p>
<!--more-->
<h2 id="what-is-largest-contentful-paint">What is Largest Contentful Paint?</h2>
<p>Largest Contentful Paint (LCP) is a Core Web Vitals metric, it represents how quickly the main content of a web page is loaded. LCP measures the time from when the user initiates loading the page until the largest image or text block is rendered in the viewport.</p>
<h2 id="how-to-measure-lcp">How to measure LCP?</h2>
<p>There are a few tools that can help with the measurement. Depending on whether your web page has user interaction or not can help determine which tools you should use to measure LCP score.</p>
<p>The following tools rely on real users loading and interacting with the page, known as <strong>Field data</strong>:</p>
<ul>
<li><a href="https://developer.chrome.com/docs/crux/">Chrome User Experience Report</a></li>
<li><a href="https://pagespeed.web.dev">PageSpeed Insights</a></li>
<li><a href="https://support.google.com/webmasters/answer/9205520">Search Console (Core Web Vitals report)</a></li>
<li><a href="https://github.com/GoogleChrome/web-vitals">web-vitals JavaScript library</a></li>
</ul>
<p>On the other hand, using these tools will simulate a page load in a consistent, controlled environment, known as <strong>Lab data</strong>:</p>
<ul>
<li><a href="https://developer.chrome.com/docs/devtools/">Chrome DevTools</a></li>
<li><a href="https://developer.chrome.com/docs/lighthouse/overview/">Lighthouse</a></li>
<li><a href="https://pagespeed.web.dev">PageSpeed Insights</a></li>
<li><a href="https://www.webpagetest.org">WebPageTest</a></li>
</ul>
<blockquote>
<p>NOTE: Sometimes results between Field data and Lab data might be different <a href="https://web.dev/articles/lab-and-field-data-differences?hl=en#lab_data_versus_field_data">to understand why check this link out</a>.</p>
</blockquote>
<h2 id="lcp-score">LCP Score</h2>
<p>Before Lighthouse v6 we only had one score for Mobile and Desktop, <a href="https://developer.chrome.com/docs/lighthouse/performance/performance-scoring?hl=en#desktop">this</a> led to artificially inflated desktop scores because all score curves were based on mobile performance data.</p>
<p>Since Lighthouse v6 this was fixed by using specific <a href="https://github.com/GoogleChrome/lighthouse/blob/138eaf000200e703d34011daa72bc0c32c5ee242/core/audits/metrics/largest-contentful-paint.js#L37-L64">desktop scoring</a> and we have two different scores:</p>
<p><strong>Mobile</strong></p>
<center><img src="/blog/assets/images/lcp/mobile.png" alt="LCP Mobile Score" /></center>
<p><strong>Desktop</strong></p>
<center><img src="/blog/assets/images/lcp/desktop.png" alt="LCP Desktop Score" /></center>
<h2 id="how-to-optimize-it">How to optimize it?</h2>
<p>Optimizing LCP might turn out to be a complex task, with complex tasks it’s generally better to break them down into smaller, more manageable tasks and address each separately.</p>
<p><strong>Server</strong></p>
<p>For a well-optimized page, having a server that responds as fast as it receives the request is crucially important to having a low LCP.
This might include writing good queries, avoiding N+1, and not running heavy tasks per request, <a href="https://www.fastruby.io/blog/performance/rails/writing-fast-rails.html">here are some tips</a></p>
<p><strong>Render-blocking</strong></p>
<p>Some scripts, stylesheets, and third-party packages, can block the process of displaying the web page causing delays in loading the content. To optimize it, use the <a href="https://web.dev/articles/fetch-priority#summary">fetch priority API</a> or deliver the static assets using a CDN, eliminate third-party JavaScript (when possible), and optimize the bundle size.</p>
<p><strong>Resources</strong></p>
<p>Images, videos, and backgrounds, it’s important to make sure the application is using the right tags, image sizes, and the best formats, <a href="https://www.fastruby.io/blog/performance/optimizing-images.html">click for more about optimizing images</a>.</p>
<p><strong>Client-site rendering</strong></p>
<p>Using client-side rendering can result in slower loading times, especially when it is loaded for the first time. Use a cache to avoid requests each time the user reloads the page.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In conclusion, optimizing LCP can be summarized in four steps:</p>
<ul>
<li>Ensure the LCP resource starts loading as early as possible.</li>
<li>Ensure the LCP element can be rendered as soon as its resource finishes loading.</li>
<li>Reduce the load time of the LCP resource as much as you can without sacrificing quality.</li>
<li>Deliver the initial HTML document as fast as possible.</li>
</ul>
<p>Ready to take your application’s performance to the next level? <a href="https://www.fastruby.io/tune">Send us a message!</a></p>juanIs your goal to rank first on Google? Have you already tried using the best keywords and strategies to rank higher but none of that has worked? It might be because your LCP, or Largest Contentful Paint, score is high and needs improvement.ReadyTech Accelerates Revenue Growth, Cuts Infrastructure Costs 5-10% with Ruby on Rails Upgrade2024-02-02T05:00:00-05:002024-02-02T05:00:00-05:00https://fastruby.io/blog/readytech-case-study<p>Based in Sydney, Australia, <a href="https://www.readytech.com.au/">ReadyTech</a> combines technology with a people-centric approach to help organisations navigate
complexity and deliver meaningful outcomes. With more than 500 employees globally, the publicly-traded company continues
to grow rapidly and now serves 4,000+ customers across three vertical segments: Education & Work Pathways, Workforce Solutions, and Government & Justice.</p>
<!--more-->
<h2 id="here-was-our-challenge">Here was our challenge</h2>
<p>Within the Education & Work Pathways business, ReadyTech builds mission-critical systems – some of which help vocational
education training institutes manage student cohorts and deliver exceptional learning experiences, as well as technology
that helps employment service providers to manage and support job seekers. The company has built most of its solutions
using Ruby on Rails. But in the midst of responding to persistent customer demand for new solutions and features, ReadyTech’s
Rails version was no longer fit-for-purpose.</p>
<blockquote>
<p>“We were acutely aware of that problem, but it was difficult to decide between addressing the tech debt and developing
new features,” said James Diamond, Chief Executive, Education & Work Pathways.</p>
</blockquote>
<p>Since feature development is easier to tie to revenue generation - and often critical to signing a new customer - that’s
where the engineering team focused its energy.</p>
<p>Over time, the existing Rails version began to pose some business challenges. It hampered engineering productivity, and
prevented the company from using newer Ruby gems that could shortcut the process of developing new functionality. And with
infrastructure expenses directly tied to AWS run time, it was proving costly to operate slower, inefficient versions of
both Rails and Ruby.</p>
<p>ReadyTech saw great value in partnering with a Ruby on Rails specialist to tackle the tech debt.</p>
<blockquote>
<p>“OmbuLabs (developers of the FastRuby.io suite of productized services) has a lot of expertise in Rails and maps out
exactly what needs to be done,” James said. “That made us confident we could outsource the upgrade and keep our team
focused on feature development work that drives growth and requires our domain expertise.”</p>
</blockquote>
<p>ReadyTech contracted with us in late 2021 to upgrade Rails for one application, then moved ahead with many more upgrades
throughout a nearly two-year engagement. “Based on how well the initial test went, working with OmbuLabs was a no-brainer,”
according to James.</p>
<p>Our client identified several vital goals for the project:</p>
<ul>
<li>Accelerate revenue growth by fast-tracking product development using the newest Ruby gems</li>
<li>Reduce infrastructure costs by improving system performance by 5% or more</li>
<li>Increase the stability of mission-critical applications that support a huge number of students, apprentices, and job seekers</li>
<li>Improve talent acquisition, retention, and morale by equipping engineers with modern development tools</li>
</ul>
<h2 id="heres-how-we-solved-it">Here’s how we solved it</h2>
<p>Using an efficient, staggered schedule, we completed smooth and non-disruptive upgrades for many applications throughout
a complex, multi-year project involving a lot of code changes and pull requests. As the ReadyTech engineering team tested
one upgrade and prepared it for production, we began work on the next upgrade. Strong communication, thorough documentation,
and weekly meetings—always accommodating time zone differences—kept the effort on track and ensured everyone stayed informed.</p>
<p>The ReadyTech team was especially impressed with our structured approach to the engagement—from following proven processes,
to mapping out an organized project journey, to setting standards and demonstrating best practices they can learn from and
apply going forward.</p>
<blockquote>
<p>“If we want to do an upgrade in-house in the future, we know that this is how the experts do it,” James said.</p>
</blockquote>
<p>While training the team on an effective upgrade process wasn’t an initial project goal, he views it as a very beneficial outcome.</p>
<p>The ReadyTech engineering team also appreciated our openness and transparency throughout the journey.</p>
<blockquote>
<p>“Whenever we had a management catch-up, they were always well prepared, detail-oriented, and willing to share their wisdom
and ideas on how we could do things better,” James said.</p>
</blockquote>
<h2 id="see-the-results">See the results</h2>
<p>Across every goal that ReadyTech sought to achieve, the upgrade project delivered.</p>
<blockquote>
<p>“Now we’re more stable, we’re moving faster, our engineering morale is better, and our infrastructure costs are going down,”
James said. “The business benefits we identified at the start of this project came to fruition.”</p>
</blockquote>
<p>For a company that invests a significant chunk of budget on technology infrastructure, a 5% average reduction in infrastructure costs - with some
applications even driving reductions of 10% - translates to significant savings.</p>
<p>The improved velocity of developing new functionality—aided by the ability to leverage the latest Ruby gems and libraries - is
boosting ReadyTech’s efficacy, enabling the company to deliver value faster and help customers scale better.</p>
<blockquote>
<p>“If we didn’t partner with OmbuLabs, we would have slowed our revenue growth because we would have slowed our product
development. We’ve got more opportunities now because we’re developing our product roadmap faster, getting to market
more quickly, and recognising revenue sooner.”</p>
</blockquote>
<p>While the labor market constantly evolves as economic conditions change, James credits the Rails upgrade as a key factor
in reducing costly staff churn. “Morale improved by virtue of being on a more current version, and it demonstrated that
we were willing to invest in continuous improvements,” he said.</p>
<p>James cites our deep expertise, pace of work, and partnership approach as three keys to the project’s success.</p>
<blockquote>
<p>“We were able to access expertise that would have been difficult for us to acquire,” he said.</p>
</blockquote>
<p>The momentum achieved by having a team dedicated to the upgrade moved the process along much faster than ReadyTech could have
on its own. And gaining a true upgrade partner gave the company assurance every step of the way. “OmbuLabs was always
willing to share and collaborate and debate with us – they just felt like part of the team,” he concluded.</p>
<h2 id="project-type">Project type</h2>
<ul>
<li>Ruby on Rails Upgrade</li>
</ul>
<h2 id="who-we-are">Who we are:</h2>
<p>OmbuLabs is Philadelphia’s lean software boutique and creators of the <a href="https://www.fastruby.io/our-services">FastRuby.io</a> suite of productized services, including
application upgrades to a secure, supported Rails version. Specializing in Ruby, JavaScript, minimum viable product development,
and reducing technical debt, we help startups to Fortune 500 companies build and improve their digital products. 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. To learn more, visit us at <a href="https://www.ombulabs.com">OmbuLabs.com</a>.</p>etagwerkerBased in Sydney, Australia, ReadyTech combines technology with a people-centric approach to help organisations navigate complexity and deliver meaningful outcomes. With more than 500 employees globally, the publicly-traded company continues to grow rapidly and now serves 4,000+ customers across three vertical segments: Education & Work Pathways, Workforce Solutions, and Government & Justice.