Validating ActiveRecord Backlinks exist

Whenever a new association is added usually we also need the opposite association to ensure things get cleaned up properly during deletion.
To never forget this and audit the current state, these two tests can help.

  def all_models
    models = Dir["app/models/**/*.rb"].grep_v(/\/concerns\//)
    models.size.must_be :>, 20
    models.each { |f| require f }
    ActiveRecord::Base.descendants
  end

  it "explicity defines what should happen to dependencies" do
    bad = all_models.flat_map do |model|
      model.reflect_on_all_associations.map do |association|
        next if association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
        next if association.options.key?(:through)
        next if association.options.key?(:dependent)
        "#{model.name} #{association.name}"
      end
    end.compact
    assert(
      bad.empty?,
      "These associations need a :dependent defined (most likely :destroy or nil)\n#{bad.join("\n")}"
    )
  end

  it "links all dependencies both ways so dependencies get deleted reliably" do
    bad = all_models.flat_map do |model|
      model.reflect_on_all_associations.map do |association|
        next if association.name == :audits
        next if association.options.fetch(:inverse_of, false).nil? # disabled on purpose
        next if association.inverse_of
        "#{model.name} #{association.name}"
      end
    end.compact
    assert(
      bad.empty?,
      <<~TEXT
        These associations need an inverse association.
        For example project has stages and stage has project.
        If automatic connection does not work, use `:inverse_of` option on the association.
        If inverse association is missing AND the inverse should not destroyed when dependency is destroyed, use `inverse_of: nil`.
        #{bad.join("\n")}
      TEXT
    )
  end

Faster Page Through Reused HTML Options with Rails & JS

We render an edit page the repeats many long option lists, so we made rails only render it’s options once and made javascript reuse them, resulting in reduced render ~70% time, ~90% page size.

It resets options  when a users reload a partially filled out page, but otherwise supports keyboard and mouse navigation nicely.

 

# app/views/projects/_form.html.erb
<% list = [["Foo", "foo"], ["Bar", "bar"], ["Name", @project.name]] %>
  <%= form.reused_select :name, list %>
  <%= form.reused_select :name, list %>
  <%= form.reused_select :name, list %>

# config/initializers/reused_select.rb
ActionView::Helpers::FormBuilder.class_eval do
  def reused_select(column, values, options={})
    value = object.public_send(column)
    options_id = "options_id-#{values.object_id}"
    options[:html] = (options[:html] || {}).merge("data-selected": value, "data-options-id": options_id)
    placeholder_values = [values.detect { |_, v| v == value }] # make select look normal
    rendered = select column, placeholder_values, options

    # render the real values only once and reuse them via js
    if @template.instance_eval { (@reused_select ||= Set.new).add? options_id }
      rendered << @template.tag(:span, id: options_id, style: "display: none", data: {options: [["", ""]] + values})
    end
    rendered
  end
end

# app/assets/javascripts/application.js
function reuseSelect(e){
  var select = e.target;
  var $select = $(select);
  var options_id = $select.data("options-id");
  var selected = $select.data("selected"); // values come from json, so be careful to match the type
  select.innerHTML = ''; // clear out fake options
  $($("#" + options_id).data("options")).each(function(_, e){
    var name = e[0];
    var value = e[1];
    var option = document.createElement("option");
    option.innerText = name;
    option.value = value;
    if(value === selected) { option.selected = "selected"; }
    select.appendChild(option);
  });
}

$("select[data-options-id]").one("mousedown", reuseSelect).one("focus", reuseSelect);

 

Automated Sudo Password Prompt with SshKit

Basically what sshkit-sudo gem promises, but:

  • 1 hack instead of multiple layers
  • obvious how to debug
  • 1 global password
  • does not capture the password prompt
  • does not print the output when capturing
  • works when not using SshKit::DSL

Hint: You might want to start with an extra “puts data” to see how your password prompt looks like.

SSHKit::Command.prepend(Module.new do
  def on_stdout(channel, data)
    if data.include? "[sudo] password for "
      @@password ||= `echo password: 1>&2 && read -s PASSWORD && printf \"$PASSWORD\"`
      channel.send_data(@@password + "\n")
    else
      super
    end
  end
end)

on servers do
  capture :sudo, "ls"
end

Datadog: Show Brittle Monitors

With the help of datadogs unofficial search_events endpoint we can see which of our monitors fail the most, a great place to start when trying to reduce alert spam.
(using Kennel)

rake brittle TAG=team:foo
analyzing 104 monitors ... 10.9s
Foo too high🔒
https://app.datadoghq.com/monitors/12345
Frequency: 18.95/h
success: 56x
warning: 44x

 

desc "Show how brittle selected teams monitors are TAG="
task brittle: "kennel:environment" do
  monitors = Kennel.send(:api).list("monitor", with_downtimes: false, monitor_tags: [ENV.fetch("TAG")])
  abort "No monitors found" if monitors.empty?

  hour = 60 * 60
  interval = 7 * 24 * hour
  now = Time.now.to_i
  max = 100

  data = Kennel::Progress.progress "analyzing #{monitors.size} monitors" do
    Kennel::Utils.parallel monitors do |monitor|
      events = Kennel.send(:api).list("monitor/#{monitor[:id]}/search_events", from_ts: now - interval, to_ts: now, count: max, start: 0)
      next if events.empty?

      duration = now - (events.last.fetch(:date_detected) / 1000)
      amount = events.size
      frequency = amount * (hour / duration.to_f)
      [monitor, frequency, events]
    end.compact
  end

  # spammy first
  data.sort_by! { |_, frequency, _| -frequency }

  data.each do |m, frequency, events|
    groups = events.group_by { |e| e.fetch(:alert_type) }
    groups.sort_by(&:first) # sort by alert_type

    puts m.fetch(:name)
    puts "https://zendesk.datadoghq.com/monitors/#{m.fetch(:id)}"
    puts "Frequency: #{frequency.round(2)}/h"
    groups.each do |type, grouped_events|
      puts "#{type}: #{grouped_events.size}x"
    end
    puts
  end
end