Ruby / Rails: clone vs. dup vs. deep_dup
Ruby and Rails have several methods for creating a new object that looks like another: clone
, dup
, deep_dup
. When using them you should be aware of their differences so that you can select the method you really need.
clone
- Shallow copy: references to other objects/values are copied (instead of cloning those objects/values)
- Clones the object and all its "special object attributes" like
frozen
,tainted
and modules that the object has been extended with - [Ruby 2.6 documentation for clone](https://devdocs.io/ruby~2.6/obj...
Dealing with I18n::InvalidPluralizationData errors
When localizing model attributes via I18n you may run into errors like this:
I18n::InvalidPluralizationData: translation data { ... } can not be used with :count => 1. key 'one' is missing.
They seem to appear out of the blue and the error message is more confusing than helpful.
TL;DR A model (e.g. Post
) is lacking an attribute (e.g. thread
) translation.
Fix it by adding a translation for that model's attribute (attributes.post.thread
). The error message reveals the (wrongly) located I18n data (from `attributes.thread...
A different testing approach with Minitest and Fixtures
Slow test suites are a major pain point in projects, often due to RSpec
and FactoryBot
. Although minitest
and fixtures are sometimes viewed as outdated, they can greatly improve test speed.
We adopted a project using minitest and fixtures, and while it required some initial refactoring and establishing good practices, the faster test suite was well worth it! Stick with me to explore how these tools might actually be a good practice.
So, why is this setup faster? Partially, it's because minitest is more lightweight than RSpec
, which...
Running Cucumber deletes my whole app!
If a Cucumber run deletes your application directory, an integration fail between Capybara and Capybara Screenshot may be the cause. Capybara Screenshot defaults to storing screenshots in .
, and tidying up screenshots happens to "tidy up" the application as well. Seen with Capybara 2.18 and Capybara Screenshot 1.0.4.
Upgrading Capybara Screenshot to 1.0.26 fixes the issue. Consider also setting Capybara.save_path = 'tmp/capybara'
in an initializer.
Timeouts for long-running SQL queries
While the main goal always is to prevent long-running queries in the first place, automatic timeouts can serve as a safety net to terminate problematic queries automatically if a set time limit is exceeded. This prevents single queries from taking up all of your database’s resources and reduces the need for manual intervention that might destabilize or even crash the application.
As Rails does not set a timeout on database statements by default, the following query will run for an entire day:
ActiveRecord::Base.connection.execute("S...
Ruby: Replacing Unicode characters with a 7-bit transliteration
Sometimes you need to remove high Unicode characters from a string, so all characters have a code point between 0 and 127. The remaining 7-bit-encoded characters ("Low-ASCII") can be transported in most strings where escaping is impossible or would be visually jarrring.
Note that transliteration this will change the string. If you need to preserve the exact string content, you need to use escaping.
Using ActiveSupport
ActiveSupport comes with a `#tran...
Be careful when copy & pasting code from the web to your terminal
What you copy may not be what you see in the browser.
Here is an online tool to determine the exact code points of your Unicode string: https://devina.io/unicode-analyser
RSpec: Increase readability with super_diff
When handling nested hashes the RSpec output is often hard to read. Here the gem super_diff
could help.
Add super_diff to your project
- Add
super_diff
to your Gemfile:
gem 'super_diff'
- Require it in your
spec_helper.rb
require 'super_diff/rspec' # For Rails applications you can replace this with 'super_diff/rspec-rails'
-
Customize colors in
spec/support/super_diff.rb
SuperDiff.configure do |config|
config.ac...
Don't compare datetimes with date ranges in MySQL and PostgreSQL
When selecting records in a date range, take care not to do it like this:
start_date = Date.parse('2007-05-01')
end_date = Date.parse('2007-05-31')
LogItem.where(:created_at => start_date .. end_date)
The problem is that created_at
is a datetime (or Time
in Ruby), while start_date
and end_date
are simple dates. In order to make sense of your query, your database will cast your dates to datetimes where the time component is 00:00:00
. Because of this the query above will lose records created from `2007-05-31 00:00:0...
Chrome Lighthouse
Chrome has a built-in utility to check performance and accessibility (and more) of your web app: Lighthouse.
Open the Developer Tools and go to the lighthouse tab:
Then you'll see some suggestions on how to improve your site.
This is cool, because you can even use it with non-public pages or your development environment (but be aware that some settings we're using for development, like not minifying JS and CSS files, might ruin your stats)...
Using tig
tig
is a command line explorer for Git that is just awesome. Install via apt-get
or brew
.
Handy commands
-
t
("tree"): Directory-structure based access. You'll see the current directory annotated with the latest change date and its author. Navigate with arrow keys or vim. -
b
("blame"): Opens the file under the cursor and annotates each line with change date and author. -
d
("diff"): LikeENTER
on a commit, but arrow keys will scroll the diff! -
/
: Search current view (e.g. commit list, diff). Jump to next hit withn
....
Learn how to use ruby/debug
This talk shows simple and advanced usages of the ruby/debug debugger. It goes through a step by step debugging workflow.
Here are some command examples:
(rdbg) step 2 # step twice
(rdbg) info # show current scope, including self
(rdbg) bt # show backtrace
(rdbg) frame 3 # go directly to frame 3
(rdbg) break User#email # add a breakpoint in the email instance method
(rdbg) catch SomeException # break when SomeException is raised
Some advanced exam...
Open Terminator from nautilus context menu
On our Ubuntu machines we have nautilus file manager with nautilus-extension-gnome-terminal
installed. This adds an entry to the context menu (right click) to start a gnome-terminal in the current directory. As I'm mostly using Terminator terminal, I wanted to have a similar context menu entry to launch Terminator directly. I came across this python script that does exactly that.
- Install python3-nautilus:
sudo apt install python3-nautilus
- Create `/usr/share/nautilus-...
Using the ActiveSupport::BroadcastLogger
The ActiveSupport::BroadcastLogger allows you to log to multiple sinks. You know this behavior from from the rails server
command, that both logs to standard out and the log/development.log
file.
Here is an example from the ActiveSupport::BroadcastLogger
API:
stdout_logger = ActiveSupport::Logger.new(STDOUT)
file_logger = ActiveSupport::Logger.new("development.log")
broadcast = ActiveSupport::BroadcastLogger.new(stdout_logger, file_logger)
broadcast.i...
High-level data types with "composed_of"
I recently stumbled upon the Rails feature composed_of. One of our applications dealt with a lot of addresses and they were implemented as 7 separate columns in the DB and Rails models. This seemed like a perfect use case to try out this feature.
TLDR
The feature is still a VERY leaky abstraction. I ran into a lot of ugly edge cases.
It also doesn't solve the question of UI. We like to use
simple_form
. It's currently not possible to simply write `f...
How to allow testing beforeunload confirmation dialogs with modern ChromeDrivers
Starting with ChromeDriver 127, if your application displays a beforeunload
confirmation dialog, ChromeDriver will immediately close it. In consequence, any automated tests which try to interact with unload prompts will fail.
This is because ChromeDriver now follows the W3C WebDriver spec which states that any unload prompts should be closed automatically.
However, this applies only to "HTTP" test sessions, i.e. what you're using by default. The spec also defines that bi-directional test se...
Optimizing images for the web
For webpages to load fast it's recommended to optimize images. Ideally an image's file size should be as small as possible while still being of decent quality. This card demonstrates two command line tools for image optimization
Use identify
to fetch information about pictures. convert
can change size/quality and strip meta information. Both commands are supplied by ImageMagick.
$ identify in.jpg
in.jpg JPEG 294x440 294x440+0+0 8-bit sRGB 92.8KB 0.000u 0:00.000
$ convert in.jpg -resize x220 -strip -set profile sRGB2014.icc -qua...
Don't require files in random order
A common pattern in Ruby is to to require all files in a specific diretory, using something like
Dir.glob(Rails.root.join('lib/ext/**/*.rb')).each do |filename|
require filename
end
However, this causes files to be required in an order determined by the file system. Since load order can be important, this may lead to different behavior on different machines which are hard to debug.
Simply add a .sort
:
Dir.glob(Rails.root.join('lib/ext/**/*.rb')).sort.each do |filename|
require filename
end
Ruby 3
...
Git: How to configure git to push only your current branch
You can change which branches will be pushed when saying git push
. Our recommendation is to set it to current
.
From the git-config
documentation:
push.default
Defines the action git push should take if no refspec is given on the command line, no refspec is configured in the remote, and no refspec is implied by any of the options given on the command line. Possible values are:
nothing
- do not push anything.- `matchin...
Rails: How to stub the env in Rails 7+
Rails 7.1 added a new method Rails.env.local?
. If you want to stub the Rails env correctly, use ActiveSupport::EnvironmentInquirer
like this:
# check if the value of stubbed_env is valid
allow(Rails).to receive(:env).and_return(ActiveSupport::EnvironmentInquirer.new(stubbed_env.to_s))
esbuild: Compressing JavaScript harder with Terser
esbuild comes with a minifier that is good enough for most cases. If you're looking to squeeze out as many bytes as possible, you can consider compressing with Terser instead.
Using Terser will increase your build times significantly, but produce the smallest output:
| | Terser (3 pass) | Terser (1 pass) | esbuild |
|----------------------------|-----------------------|------------------|-------...
A simple example with a GIN index in Rails for optimizing a ILIKE query
You can improve your LIKE
/ ILIKE
search queries in PostgreSQL by adding a GIN index with an operate class ("opclass") to split the words into trigrams to the required columns.
Example
class AddSearchTextIndexToUsers < ActiveRecord::Migration[7.1]
def change
enable_extension 'pg_trgm'
add_index :users, :search_tex...
Logging multiple lines in Rails without making filtering your logs difficult
Rails' default logger prefixes each log entry with timestamp and tags (like request ID).
For multi-line entries, only the first line is prefixed which can give you a hard time when grepping logs.
Example
Rails.logger.info(<<~TEXT)
Response from example.com:
Status: 200
Body: It works!
TEXT
With that, the following is written to your log file.
I, [2024-10-04T08:12:16.576463 #1917250] INFO -- : [97e45eae-a220-412d-96ad-e9e148ead71d] Response from example.com:
Status: 200
Body: It works!
If you then run `grep...
How to: Benchmark an Active Record query with a Ruby script
Recently I needed to benchmark an Active Record query for performance measurements. I wrote a small script that runs each query to benchmark 100 times and calculates the 95th percentile.
Note: The script requires sudo permissions to drop RAM cache of PostgreSQL. Due to the number of iterations it was impractical to enter my user password that often. And I temporary edited my /etc/sudoers
to not ask for the sudo password with johndoe ALL=(ALL) NOPASSWD: ALL
.
# Run this script with e.g. `rails ru...