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 a operate class 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_text, using: :gi...
Preventing new lines in your Rails logs helps you to grep the logs afterwards
Using the default Rails logger with an plain text output does not work well with multi line logs. Try to call the logger method multiple times to make a request easier to grep e.g. by a request id.
Example bad
Rails.logger.info(<<~TEXT)
Response from example.com:
Status: 200
Body: It works!
TEXT
I, [2024-10-04T08:12:16.576463 #1917250] INFO -- : [97e45eae-a220-412d-96ad-e9e148ead71d] Response from example.com:
Status: 200
Body: It works!
grep 97e45eae-a220-412d-96ad-e9e148ead71d log/production.log
would re...
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...
Your Rails sandbox console
Just found out about a great feature in Rails that seems to be around since Rails 2. Start a console with the --sandbox
(or -s
) parameter:
rails console --sandbox
All changes you make to the database will be rolled back on exit.
Warning
Changes beyond the database (deleting files, sending emails, etc) cannot be rolled back!
Understanding race conditions with duplicate unique keys in Rails
validates_uniqueness_of
is not sufficient to ensure the uniqueness of a value. The reason for this is that in production, multiple worker processes can cause race conditions:
- Two concurrent requests try to create a user with the same name (and we want user names to be unique)
- The requests are accepted on the server by two worker processes who will now process them in parallel
- Both requests scan the
users
table and see that the name is available - Both requests pass validation and create a user with the seemingly available name...
Devise: Don't forget to lock users with soft delete
There are two ways to lock a user in devise.
- Using the lockable module
- Customizing the user account status validation when logging in.
It depends on your requirements which methods works best.
Locking a user on soft delete
We recommend to use option 2 when you want to couple the lock to the m...
Terser is good at minifying JavaScript
Terser is a really good minifier ("compressor") for JavaScript code. I'm often surprised by the thoughtfulness of its compressed output.
Let's take this function:
function fn() {
if (a) {
return 'foo'
} else if (b) {
return 'foo'
} else {
return c()
}
}
console.log(fn())
Terser will reduce this to the following code:
console.log(a||b?"foo":c())
Note how:
- The
if
statement has been replaced by a tertiary expression. This is often less readable, but it doesn't matter in c...
HTML/CSS: "transparent" is not a color
Heads up: transparent
is not a real color, but black
with 0% opacity.
In transparent gradients, this adds some gray shades:
/* ❌ Looks dull in older browsers */
linear-gradient(to right, white 0%, transparent 100%)
Browser vendors have soon found a fix, but Safari only implemented it in 15.4. Should you need to support older versions, here is how:
/* ✅ Fix for older browsers: transparent white */
linear-gradient(to right, white 0%, rgba(255,255,255,0) 100%)
PostgreSQL and its way of sorting strings
PostgreSQL uses the C library's locale facilities for sorting strings:
- First, all the letters are compared, ignoring spaces and punctuation.
- It sorts upper and lower case letters together. So the order will be something like
a A b B c C
- Then, spaces and punctuation are compared to break ties.
Example:
Ruby | PostgreSQL |
---|---|
IMAGE3.jpg | image2.jpg |
image.jpg | image3.jpg |
image2.jpg | IMAGE3.jpg |
image3.jpg | image.jpg |
Further reading
- [PostgreSQL-FAQ: Why do my strings sort incorrectly?](h...
Run all RSpec tests edited or added in the current branch
With this command you can run all the spec files which have been edited or added in the current branch since master:
git diff --name-only master -- ./spec | xargs -I{} rspec {}
- If you have several spec folders add them for path parameter after
./spec
accordingly. - The option
-I{}
creates a placeholder to be replaced. - You can also compare edited/added specs between commits with
<commit>..<commit>
Jasmine: Use `throwUnless` for testing-library's `waitFor`
testing-library are widely used testing utilities libraries for javascript dependent frontend testing. The main utilities provided are query methods, user interactions, dom expectations and interacting with components of several frontend frameworks, which allows us to worry less about the details happening in the browser and focus more on user centric tests instead!
Some of the time you will find a necessity to use methods like [waitFor
](https://testing-library.com/docs/dom-testing-library/api-async/...
Jasmine: Dealing with randomness
Whenever you have to deal with randomness in a jasmine test there are some spy strategies to help you out!
Let's say we have a method Random.shuffle(array)
to shuffle an array randomly and a class that uses shuffle within the constructor.
returnValue
& returnValues
it('shuffles the array', () => {
spyOn(Random, 'shuffle').and.returnValue([3, 2, 1])
array = [1, 2, 3]
testedClass = new testedClass(array)
expect(Random.shuffle).toHaveBeenCalled()
expect(testedClass.array).toEqual([3, 2, 1])
})
If you have...
Rails credentials: Always use the bang version
Rails credentials are a way to store secrets in an encrypted YAML file. Usage is simple: each key in the credentials file becomes a method on Rails.application.credentials
, returning the corresponding secret.
# Credentials file
file_storage_secret: superstrongsecret
# Somewhere in the application
FileStorage.secret = Rails.application.credentials.file_storage_secret
Since credentials usually are different between environments, you can easily forget to define them for another environment. If it is an API token, you'll...
Implementing a custom RuboCop cop
It's possible to implement simple custom RuboCop cops with very little code. They work exactly the same like existing rubocop cops and fail the pipeline if they find an offense. This is handy for project specific internal rules or conventions.
The following cop looks at every ruby file and searches for TODO
or WIP
comments and adds an offense.
class NoTodos < RuboCop::Cop::Base
MSG = "Don't add TODOs & WIPs in the source."
def on_new_investigation
processed_source.comments.each { |comment| search_for_forbidden_ann...
Be very careful with 301 and 308 redirects
Browsers support different types of redirects.
Be very careful with these status codes:
301 Moved Permanently
308 Permanent Redirect
Most browsers seem to cache these redirects forever, unless you set different Cache-Control
headers. If you don't have any cache control headers, you can never change them without forcing users to empty their cache.
Note
By default Rails sends a ...
Rails: Prefer parsing dates with Date.strptime()
It is very common to parse dates from strings. It seems obvious to use Date.parse
for this job. However this method does not validate the input and tries to guess the format of the string.
This can lead to a very unexpected results:
Date.parse('Foobar_09_2018')
# Tue, 09 Oct 2018
In most of the cases it would be better to use Date.strptime
as you can provide a date or time pattern to match against.
Date.strptime('Foobar_09_2018', '%d_%m_%Y')
# ArgumentError (invalid strptime format - `%d_%m_%Y')
Date.strptime('01_09...
Rails SQL Injection Examples
This page lists many query methods and options in ActiveRecord which do not sanitize raw SQL arguments and are not intended to be called with unsafe user input. Careless use of these methods can open up code to SQL Injection exploits. The examples here do not include SQL injection from known CVEs and are not vulnerabilites themselves, only potential misuses of the methods.
Please use this list as a guide of what not to do.
How to combine unknown CSS selectors
You are given two CSS selectors that you do not control. How can you build a new selector that matches both of them?
item_selector = 'div'
active_selector = '.is-active'
Can't I just concat these selectors?
# Bad
new_selector = "#{item_selector}#{active_selector}"
# => "div.is-active"
Don't! This will break as soon as one of the selectors is actually a selector list.
item_selector = 'div, span, p' # <- Selector list
new_selector # => "div, span, p.is-active" (wrong)
Solution
Wrap both selectors ...
JavaScript: Listening to a class getting added
Reacting on a class getting added can be done with a mutation observer. Example:
const items = document.querySelectorAll('.item')
const expectedClass = 'active'
const activeObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.target.classList.contains(expectedClass) {
// Do something
}
})
})
items.forEach(item => activeObserver.observe(item, { attributes: true, attributeFilter: ['class'] }))
Note that this is not a generic solution – it makes a few assumptions to simplif...
How to ask a (mobile) browser about the true visual viewport
The Visual Viewport API enables developers to access the actually visible area of the page. This differs from the normal viewport if:
- the user has pinch-zoomed
- the on-screen keyboard is visible
- there are other page-independent artifacts
Obtain a VisualViewport
from window.visualViewport
. The object has the properties offsetLeft
and offsetTop
, and three events: resize
, scroll
, scrollend
. You can use these to place and keep an element within the visual vi...
Updated: Controlling issue grouping in Sentry
Added that usage of * wildcards might be necessary in custom fingerprinting rules.
How to start Terminator with split screens and custom commands running
Starting Terminator with split screens is quite simple: Just store a layout and start Terminator with the --layout <your layout>
option.
However, if you want to run custom commands in your terminals, you need to do some work to keep these terminals from closing after a command exits. You accomplish this by tweaking bash to run a command before actually starting.
Pimp your .bashrc
Add this to the end of .bashrc
:
# hack to keep a bash open when starting it with a command
[[ $startup_cmd ]] && { declare +x $startup_cmd; hi...