22 posts by kevin-perez
@_kevinnio

InstantClick behavior in Turbo

Today I learned about the new default InstantClick behavior for Turbo. Turbo now prefetches a link when hovering it and changes the page's contents after clicking on it, resulting in instantaneous page changes most of the time. There's a time window of ~300ms between the hover and the click event, so if you want to optimize for this make sure your backend can respond inside that time window.

This is enabled by default but you can opt out of it by adding data-turbo-prefetch="false" to specific links or to whole containers. Add it to your main container to disable it completely.

More info and demos here: https://github.com/hotwired/turbo/pull/1101

An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'

If you're working with APIs like the OpenAI one and you're passing tools to your bots, make sure the results of a tool call are always right next to the tool call message! Otherwise the API will return a 400 response. Heads up! This a recent change in their API (they use to have functions but is now deprecated) and the mandatory order is not mentioned in the official docs!

// It always needs to be like this
[
  { 
    "role": "user", 
    "content": "Tell me the current time", 
    "tools": [{ "name": "current_time", "description": "Gives you the current time" }]
  },
  {
    "role": "assistant",
    "content": "",
    "tool_calls": [{
      "type": "function",
      "id": "d8cdd56d-98e1-4dea-ae3a-2b23",
      "function": { "name": "current_time", "arguments": "{}" }
    }]
  },
  {
    "role": "assistant",
    "content": "Current time is 10:22 AM",
    "tool_call_id": "d8cdd56d-98e1-4dea-ae3a-2b23"
  },
  {
    "role": "system",
    "content": "Just a message after the tool result"
  },
]

Never do this!

[
  { 
    "role": "user", 
    "content": "Tell me the current time", 
    "functions": [{ "name": "current_time", "description": "Gives you the current time" }]
  },
  {
    "role": "assistant",
    "content": "",
    "tool_calls": [{ /* omitted */ }]
  },
  {
    "role": "system",
    "content": "Just a message between the tool call and the tool result"
  },
  {
    "role": "assistant",
    "content": "Current time is 10:22 AM",
    "tool_call_id": "d8cdd56d-98e1-4dea-ae3a-2b23"
  }
]

:use_route in controller specs inside engines

If you have controller specs inside your Rails engine, you'll need to pass use_route: :my_engine when doing the request. Otherwise you'll get an UrlGenerationError since the dummy app is not running and the engine is not mounted!

describe MyEngine::MainController, type: :controller do
  it "does something" do
    get :index, params: { use_route: :my_engine }
    # ...
  end
end

Use .slugignore to remove files before building on Heroku!

Heroku supports a .slugignore file that works similarly to a .gitignore. Heroku will remove any file referenced in it after checkout and before building a new slug.

https://devcenter.heroku.com/articles/slug-compiler#ignoring-files-with-slugignore

Reduce your slug size on Heroku!

You don't need app/javascript and node_modules after compiling assets. Enhance the assets:precompile task and delete them so they don't get added to your Heroku slug! Small slugs allow for fast boot up times and better scaling. This also helps stay below the 500MB size limit!

Rake::Task["assets:precompile"].enhance do
  next unless Rails.env.production?

  ["#{Dir.pwd}/app/javascript", "#{Dir.pwd}/node_modules"].each do |dir_path|
    FileUtils.rm_rf(dir_path)
  end
end

Find files with largest amount of lines in your project

Ever needed to find the file with the largest amount of lines in your project? Use the snippet below to list all files and their line count, neatly sorted from smallest to largest.

find . -type f -print0 | xargs -0 wc -l | sort -n

This translates to the following:

find . -type f -print0 # Find all regular files in this dir and pipe them into xargs with \0 as separators.
xargs -0 wc -l # For each file contents, count the amount of lines in it and...
sort -n # Sort them numerically.

Use OpenTelemetry gems to track your app's performance

Instead of going with expensive services like New Relic or Datadog, trace your Rails app's performance using the OpenTelemetry gems.

First, add the gems to your Gemfile:

gem 'opentelemetry-sdk'
gem 'opentelemetry-exporter-otlp'
gem 'opentelemetry-instrumentation-all'

Then, add this inside config/initializers/opentelemetry.rb

require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'

OpenTelemetry::SDK.configure do |c|
  c.service_name = '<YOUR_SERVICE_NAME>'
  c.use_all() # enables all instrumentation!
end

Finally, launch your application and point it to a collector, like the OpenTelemetry Collector, Grafana Agent or SigNoz. Most of them have cloud or self-hosted versions.

Enjoy your observability!

Run your GitHub Actions locally with Act

Act is a tool that will let you run your GitHub Actions locally, very useful when you're debugging something that only happens during CI! Just keep in mind that the tool is still not mature and you'll have to boot up your required services manually.

https://github.com/nektos/act

Upgrade to Ruby 3.2 to avoid ReDoS attacks

Did you know that in Ruby 3.1.3 and prior some regular expressions could take a long time to process?

Don't believe me? Try running this in a 3.1.3 irb console:

/^a*b?a*$/ =~ "a" * 50000 + "x"

Your system will halt for like 10 seconds before returning no matches. This is the basis for ReDoS (Regexp Denial of Service) attacks.

Thankfully, Ruby 3.2.0 has fixed this and the same regexp gets resolved in 0.003 seconds. They also added a Regex.timeout global option which would prevent your app from falling victim to ReDoS attacks!

Enumerable#any? and Enumerable#detect

Someone told me #any? didn't stop iterating after finding the first truthy value, that I should use #detect instead, but turns out both methods behave the same, at least in Ruby 3.0.1.

Try running this for example:

(1..5).to_a.any? do |n|
  puts n
  true
end

(1..5).to_a.detect do |n|
  puts n
  true
end

You'll see this:

1
1

Tell apt to keep the current version of a package

If you ever need apt to keep the current version of a package, use hold:

sudo apt-mark hold <package>

This will prevent apt from upgrading the package. You can always unhold it back when you want to upgrade it.

Always use retry inside rescue blocks

Ruby includes a retry keyword that would bring execution back to the start of the current method while preserving scope. But what if you do this?

def my_method
  my_var = 1
  retry
end

That would fail with an Invalid retry, which is one of the few illegal statements in Ruby.

retry is only allowed inside rescue blocks.

def my_method
  my_var = 1
  raise Exception
rescue Exception
  retry
end

Always check ACLs if something is fishy!

If you ever encounter a Linux dir in which you cannot write even when you're sure you have write permissions on it, check the ACLs. You may find someone doesn't particularly like you. 😞

-> getfacl /tmp 
# file: tmp
# owner: root
# group: root
# flags: --t
user::rwx
group::rwx
other::rwx
user:my_user:--- # this means "screw u and only u"

Defining classes using full name vs wrapping them inside modules

In Ruby, we can write classes inside modules. We usually do this to have classes namespaced to the module, so we don't risk two different classes having the same name. This is important since Ruby won't complain about re-defining a class, it would just open it and add new methods to it.

We could write classes using their full name, like this:

class Pokemon::Game::SpecialAttack < Pokemon::Game::Attack
  # ...
end

or, we could wrap the whole thing using modules, like this:

module Pokemon
  module Game
    class SpecialAttack < Attack # Ruby looks for SpecialAttack inside Pokemon::Game automatically!
      # ...
    end
  end
end

Both methods work the same, except the one above can only reference Attack using its whole name. On the other hand, it looks somewhat easier to read than nesting modules inside other modules. Which style do you prefer?

Upgrade your SSH keys from RSA to ED25519

ssh-rsa is the most common public/private key type, but is widely considered insecure with key lengths lower than 2048 bits. If you created your SSH key using ssh-keygen with default options a while ago, chances are you're using an unsafe key. Furthermore, support for RSA host keys (keys that identify the server you're trying to connect to) is disabled by default since OpenSSH 8.8 and they may consider disabling the algorithm altogether in the future.

But don't worry! Just create a new key for yourself using the most recommended key type available today: ED25519.

ssh-keygen -t ED25519 -a 100 -C "myemail@email.com"

Just make sure you got OpenSSH 6.5 or greater on both ends. Don't forget to install your new key and remove the old one!

Underscores are not allowed as part of domain names

According to rfc1035, underscores (_) are not allowed as part of domain names.

The labels must follow the rules for ARPANET host names. They must start with a letter, end with a letter or digit, and have as interior characters only letters, digits, and hyphen (-). There are also some restrictions on the length. Labels must be 63 characters or less.

This means domain names like my_domain.com or sub_domain.main-domain.com are invalid.

Special thanks to @jclopezdev for finding this out.

ON DUPLICATE KEY UPDATE

When you're doing an INSERT query, you could be trying to insert a row containing a primary key that already exists in the table. Instead of doing a previous query to see if the key exists or not, you could try ON DUPLICATE KEY UPDATE.

INSERT INTO table (
  field1, 
  field2
) 
VALUES (
  "foo", 
  "bar"
) 
ON DUPLICATE KEY UPDATE 
  field1="foo", 
  field2="bar"

You can forward/reverse ports on Android device using adb

I'm developing an Android app as a side project and today I learned about adb forward and adb reverse. Basically, they allow you to forward or reverse traffic to specific ports between an Android device and your development machine.

Let's say I need my app to fetch something from http://localhost:3000/some-data. When the app is running on the phone localhost refers to the phone itself, and my server is running in my dev machine. So, if I do this:

adb reverse tcp:3000 tcp:3000

Now when the app tries to access localhost:3000 it will actually reach out to my dev machine's 3000 port. Very useful if you're developing the app's backend as well (as I am).

Similarly, if I wanted to access a server inside the phone from my dev machine, I could run:

adb forward tcp:5000 tcp:5000

And now if I run curl http://localhost:5000 in my dev machine it will hit the server running on the phone. Pretty neat!

Ruby 2.7 introduced numbered parameters for blocks

Since Ruby 2.7, it is possible to use numbered parameters in blocks additionally to named parameters.

This is an example of a block with named parameters:

my_array.each { |element| element.do_something }

And this is the same line with numbered parameters:

my_array.each { _1.do_something }

This works with multiple parameters too:

# named parameters
my_hash.each_pair { |key, value| puts "#{key}: #{value}" }

# numered parameters
my_hash.each_pair { puts "#{_1}: #{_2}" }

If you're having issues installing gems...

...try running rm -rf vendor/cache inside your app's root directory. Looks like sometimes cache can cause compilation issues while building gem extensions so getting rid of it fixes the issue. I can't guarantee this works 100% of the time, but it's worth giving a try if it can help you avoid a headache.

Testing and sharing simple SQL queries online

If you want to test and share a simple SQL query, take a look at http://sqlize.com. Keep in mind you'll need to create the tables you need in the same script and you're only allowed to create TEMPORARY tables. You can run your query and see the results below it right away. Then, you can generate a permalink and share your query with everyone!

Beware of calling #count on Active Record relations!

Given code like this:

records = Record.includes(:related).all # Eager-loads to prevent N+1 queries...
records.each do |record|
  puts record.related.count # => ... but this produces N+1 queries anyway!
end

If you run this, you'll notice you get an N+1 queries problem, even though we're using #includes. This happens because of record.related.count. Remember, records.related here is not an Array but an instance of CollectionProxy and its #count method always reaches out to the database. Use #length or #size instead to solve this issue.

records = Record.includes(:related).all
records.each do |record|
  puts record.related.length # Problem solved!
end