Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby: FactoryBot.lintでFactoryの壊れないようにチェックする

FactoryBotを利用してテストデータを管理していて、開発が進んでいくとデフォルトで作成したときにエラーになるようなFactoryが作られてしまったりします。

github.com

そんなときにFactoryBot.lintを使うとFactoryが壊れたときに気づけて便利だったのでメモしておきます📝

やり方は簡単で以下のようなテストを追加するだけです。

require 'rails_helper'

RSpec.describe FactoryBot, type: :model do
  describe '.lint' do
    it 'すべてのFactoryが正常に機能すること' do
      FactoryBot.lint traits: true
    end
  end
end

上記設定だとdefault及びtraitを付与した状態でFactoryをcreateしたときにエラーが発生しないかチェックしてくれます。 ※実態はpublic_sendでstrategy(create or build)を実行してエラー発生有無を監視しているようですね👀

https://github.com/thoughtbot/factory_bot/blob/92114db048b70096183af622173506535b150e65/lib/factory_bot/linter.rb#L73

エラーが発生した場合には以下のような形でエラーになります。

  1) FactoryBot.lint すべてのFactoryが正常に機能すること
     Failure/Error: FactoryBot.lint traits: true
     
     FactoryBot::InvalidFactoryError:
       The following factories are invalid:

以下のFacotyBotのREADMEに詳しい使い方が書いてあるので気になる人は見てみてください📕

github.com

JavaScript: Rollbarのエラー通知をSnippetではなくRollbar.jsを使って行う

JavaScriptのエラー検知にRollbarを使用しているのですが、最近npmで公開されているRollbar.jsを利用するとSnipetを使わなくて済むことがわかったので利用方法をメモしておきます📝

公式ドキュメント記載のSnipetを利用する方法

公式ドキュメントのBrowserJSのQuick Startは、

docs.rollbar.com

以下のような形で記載されておあり、エラー通知用のSnipetを活用してエラーを通知するようになっています。

var _rollbarConfig = {
    accessToken: "POST_CLIENT_ITEM_ACCESS_TOKEN",
    captureUncaught: true,
    captureUnhandledRejections: true,
    payload: {
        environment: "production"
    }
};
// Rollbar Snippet
!function(r){var e={};f...
// End Rollbar Snippet

しかし、Snipetはminifiedされており、eslint等のチェックをいちいち無効化するのがめんどくさいのとバージョンアップも手動で行わないといけません。。。

npmに公開されているRollbar.jsを利用するとバージョンアップとかそのあたりをpackage.jsonで管理できそうです。

Rollbar.jsを使ったSnipetを使わずに通知を有効化する

www.npmjs.com

まずは以下のような形でRollbar.jsをinstallします。

$ yarn add rollbar
# or 
$ npm i rollbar

あとは以下のような形で利用するだけです✨

import Rollbar from "rollbar";

new Rollbar({
  accessToken: 'POST_CLIENT_ITEM_TOKEN',
  captureUncaught: true,
  captureUnhandledRejections: true,
  payload: {
      environment: "production"
  }
});

Rollbarに明示的に通知を送りたいときには以下のような感じでexportして、

import Rollbar from "rollbar";

export default new Rollbar({
  accessToken: 'POST_CLIENT_ITEM_TOKEN',
  captureUncaught: true,
  captureUnhandledRejections: true,
  payload: {
      environment: "production"
  }
}); 

以下のような形で利用できます。

import Rollbar from '@/my_exported_rollbar_path'

Rollbar.info('test rollbar post!')

やはり外部サービス系のmoduleは公式に管理されているものを利用できると便利ですね!

Ruby on Rails: Rails 6で追加された`insert_all`の実行時にデフォルトでTimestampを付与するMEMO

Rails 6でbulk insertの機能が実現されました🎉
※概要は以前紹介してるので興味のある方は参照してください。

madogiwa0124.hatenablog.com

非常に便利なのですが、記事でも記載した以下の通り、timestampが補完されずにcreate_at、updated_atにNOT NULL制約を付与していると、エラーになってしまうのが、activerecord-importからの乗り換え目的等で検討すると、やはりtimestampをデフォルトで補完したいケースもあるのかなと、、、

created_atとupdated_atを補完してくれないので自分で設定しないといけない点に注意です。実装をシンプルにしてパフォーマンスを担保するのとupdate_allの仕様に合わせているのが理由のようです🤔 https://github.com/rails/rails/issues/35493#issuecomment-470100313

github.com

なので、ちょっと対応方法を考えたのでMEMOしておきます📝

DBのDEFAULT制約を利用する

tableを作るときに以下のような形でDAFAULT制約を用いてCURRENT_TIMESTAMPを実行するようにすることでtimestampのカラムに現在時刻を自動的に設定します。

class CreateFoo < ActiveRecord::Migration[6.0]
  def change
    create_table :foo do |t|
      t.timestamps default: -> { 'CURRENT_TIMESTAMP' }
    end
  end
end

これは下記のissueでコメントされている方法です。

github.com

SQLを直接実行するのでDBMSによって多少方言が違う可能性があるのがネックですが、CURRENT_TIMESTAMPMySQLにもPostgreSQLにもSQLite実装されている関数のようなので基本的には大丈夫そうです※挙動は微妙に違うかもですが。。。

ActiveRecordにパッチを当てる

あとはActiveRecordにパッチを当ててTime.currentで取得した値を差し込むのもありかなとも思いました。

class ApplicationRecord < ActiveRecord::Base
  # NOTE: insert_all実行時にデフォルトでtimestampを付与するパッチ
  def insert_all(attributes, returning: nil, unique_by: nil, now: Time.current)
    attributes.map! { |attr| attr.merge(created_at: now, updated_at: now) }
    super
  end

  def self.insert_all!(attributes, returning: nil, now: Time.current))
    attributes.map! { |attr| attr.merge(created_at: now, updated_at: now) }
    super
  end
end

上記のコードではinsert_allでは、引数で渡されたattributesをもとにbulk insertを実行するので、created_atupdated_atTime.currentで取得した現在時刻を設定したHashをattributesにmergeしています。

https://github.com/rails/rails/blob/f855139f3d2bb9b032613279d0adfbd6a77a2d07/activerecord/lib/active_record/persistence.rb#L131-L133

以下のような形でtimestampeを指定しなくても実行出来るようになります。

Foo.insert_all([{ title: 'foo1', body: 'bar1' }, { title: 'foo2', body: 'bar3' }])

具体的には、以下のようなコードを実行しているのとほぼ同じ挙動になります。

now = Time.current
Foo.insert_all([
  { title: 'foo1', body: 'bar1', created_at: now, updated_at: now },
  { title: 'foo2', body: 'bar3', created_at: now, updated_at: now },
])

おわりに

やりすぎは注意ですが、Rubyはライブラリのコードでも柔軟に拡張出来るのが良いですね✨

Heroku:Free DynoでRailsアプリケーションとSidekiqを動かすMEMO📝

HerokuでRailsアプリケーションを運用しているのですが、Sidekiqを導入するときにredisを構築したり、sidekiqのプロセスを立ち上げたり、色々と調べることがあったので、そのあたりの内容をメモしておきます📝

pumaからhookしてsidekiqのプロセスをwebのdynoのインスタンス上で立ち上げる方法もあるようですが、今回はsidekiq用のプロセスタイプを別に定義して構築する方法を記述しています。

bilalbudhani.com

freeプランでもプロセスタイプ数は2つまで無料のようなので・・・!

jp.heroku.com

Heroku Redisの導入

Sidekiqを利用するためにHeroku Redisを使ってRedisを構築します。

devcenter.heroku.com

Heroku RedisはHobby Devであれば無料で構築できます。

導入は簡単で以下のコマンドを実行するかHerokuのダッシュボードから手動で構築することができます。

$ heroku addons:create heroku-redis:hobby-dev -a your-app-name --version your-redis-version

※デフォルトだと6.0系がインストールされるようです👀(2021/03/28時点)

構築は数分で終わって完了すると環境変数REDIS_URLに構築したHeroku Redisへの接続情報が自動的に設定されます。(便利✨)

As of Redis version 6, Hobby plans support both TLS and unencrypted connections, while production plans require TLS connections.

https://devcenter.heroku.com/articles/heroku-redis#create-a-new-instance

redis 6以降のredisを構築すると、REDIS_TLS_URLも同時に環境変数に定義され、こちらを利用するとTLSを活用した暗号化もサポートされているようです👀

sidekiqの導入

SidekiqのWiki等を参考にgemをインストールします。

gam 'sidekiq'

私は以下のような設定ファイルをconfig/sidekiq.ymlに置きましたが、このへんは好みなので設置せずに実行時に指定するなり、諸々対応します。

---
:concurrency: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
:queues:
  - high
  - default
  - low
development:
  :logfile: log/sidekiq.log
  :pidfile: tmp/pids/sidekiq.pid

あとはroutesにSidekiqの管理画面をmountして、initializerの中でベーシック認証を追加しておきます。

Rails.application.routes.draw do
  require 'sidekiq/web'
  mount Sidekiq::Web, at: '/sidekiq'
end
require 'sidekiq/web'

# NOTE: Sidekiqのwebuiにベーシック認証をかける
Sidekiq::Web.use(Rack::Auth::Basic) do |user, password|
  [user, password] == [ENV['BASIC_AUTH_USER'], ENV['BASIC_AUTH_PASSWORD']]
end

※ SidekiqはデフォルトでREDIS_URLを参照してくれるので接続情報等の設定はなくても大丈夫です。

You may also use the generic REDIS_URL which may be set to your own private Redis server. Using Redis · mperham/sidekiq Wiki · GitHub

Sidekiq用のプロセスタイプを定義

プロジェクトのルートに以下を記載したProcfileを用意してSidekiq用のworkerのプロセスタイプを定義し、herokuにpushします。

web: bin/rails server -p ${PORT:-5000} -e $RAILS_ENV
worker: bundle exec sidekiq

HerokuはこのProcfileに記載されたプロセスタイプごとにインスタンスを生成するようです。

devcenter.heroku.com

sidekiq用のインスタンスを構築する。

以下のコマンドを実行し、HerokuにRailsアプリケーションが動作するwebとSidekiqが動作するworkerインスタンスを1つずつ立ち上げます。

$ heroku ps:scale web=1 worker=1

※ 前述の「 Sidekiq用のプロセスタイプを定義」を実施しないとコマンド実行時に以下のエラーが発生します。前述の通り。Herokuはプロセスタイプに従ってインスタンス化するようなので、事前にProcfileにプロセスタイプを定義する必要があるようです👀

$ heroku ps:scale web=1 worker=1
Scaling dynos... !
 ▸    Couldn't find that process type (worker).

これでHerokuでRailsアプリケーションとSidekiqを起動することができました🙌

以下のようなコマンドを実行してJobをエンキューして無事に処理されることを確認できれば問題なさそうです。

$ heroku run bin/rails c -e production
$ MySampleJob.perform_later

おわりに

Herokuの無料プランでSidekiqまで動かせると思っていなかったのですが、ここまで出来るとは・・・!

参考

yutojp.com

uesaiso.netlify.app

sunday-morning.app

Ruby: RubyCriticでコードを静的解析して品質を計測するMEMO📝

個人で運用しているRuby on Rails製のWebサービスRubyCriticを使ってCIで品質レポートを取得するようにしたので、そのあたりのやり方等をメモしておきます📝

rubycriticとは?

RubyCritic is a gem that wraps around static analysis gems such as Reek, Flay and Flog to provide a quality report of your Ruby code.

上記の通り複数の静的解析ツールを活用していい感じのレポートを作成してくれるgemです💎

github.com

実行すると以下のようなレポートを作成してくれます。

f:id:madogiwa0124:20210320154053p:plain

※htmlだけではなくjson等も出力可能のようです

RubyCriticを導入する

以下をGemfilleに追記してbundle installすればRubyCriticが使用出来るようになります。

gem "rubycritic", require: false 

しかしデフォルトだとすべてのファイルが対象になってしまいライブラリのコードも検査してしまうので、実行時に--pathで任意のディレクトリを指定するか、プロジェクトのルートに.rubycritic.ymlを作成すると良さそうです。

paths:
  - 'app/'
  - 'lib/'

RubyCriticで特定のルールを無効化する

RubyCriticで特定のルールを無効化する場合には、RubyCriticが利用するツールに沿って無効化してあげる必要があります。

例えばreekが検知するIrresponsibleModuleを無効化したい場合には、プロジェクトのルートディレクトリに以下のような.reek.ymlを追加してあげると無効化することができます。

detectors:
  IrresponsibleModule:
    enabled: false

github.com

RubyCriticをCIで実行する

CircleCIの場合は以下のようなcommandを作ってあげると良さそうです、RubyCriticを実行して結果をartifactで永続化しています。

commands:
  store_atifacts:
    parameters:
      path:
        type: string
    steps:
      - store_artifacts:
          path: << parameters.path >>
  run_and_store_rubycritic:
    steps:
      - run:
          name: run rubycritic
          command: bundle exec rubycritic 
      - store_atifacts:
          path: tmp/rubycritic

おわりに

個人での開発だとレビューとかあまり無いので、こういうツールを積極的に活用していきたいですね!!

参考

github.com

Ruby: Deviseで現在有効になっているOmniAuthのproviderのリストを取得する

Deviseで現在の環境で有効になっているproviderを確認して有効なものだけのログインボタンを出す(例:開発環境でだけdeveloperストラテジを有効化しているので開発環境でだけ導線を出したい) 等の場合に現在有効なproviderを取得したいときのやり方をMEMOしておきます✍

deviseのバージョンは4.7.1です。

やりかた

以下でDeviseで現在有効になっているOmniAuthのproviderのリスト取得することが出来ます。

Devise.omniauth_providers
#=> [:developer, :twitter]

該当ソースは以下

https://github.com/heartcombo/devise/blob/45b831c4ea5a35914037bd27fe88b76d7b3683a4/lib/devise.rb#L329-L331

callback先とかも含めた全体的な情報は以下で取得出来るようです👀

Devise.omniauth_configs

https://github.com/heartcombo/devise/blob/45b831c4ea5a35914037bd27fe88b76d7b3683a4/lib/devise.rb#L271-L272

便利✨

Ruby: FactoryBotでglobalに使えるtraitを定義するMEMO

以下の記事を追記するときにFactoryBotでfactory跨いで使いたいtraitを定義する方法調べていて見つけたのでMEMOしておきます📝

madogiwa0124.hatenablog.com

検証したバージョンはfactory_bot 6.1.0です。

やりたいこと

以下のようなstrict loadingを使用したN+1を検知する設定 + strict laodingを無効化するためのメソッドを用意した実装を行っているとします。

class ApplicationRecord < ActiveRecord::Base
  self.strict_loading_by_default = true

  def strict_loading!(value = true)
    @strict_loading = value
  end
end

上記を行ったけどテストコードではstrict loadingを一時的に無効化したい場合にtraitを使ってシンプルにかけるようにしたいというのがやりたいことになります。

describe '#foo' do
  let(:foo) { create(:foo, :strict_loaded) }

  before do 
    create(:bar, foo: foo)
  end

  it { expect(foo.bars.length).to eq 1 }
end

各factoryで個別にtraitを書くのは手間なのでglobalに以下のようなtraitを定義したいものです。

  trait :strict_loaded do
    after(:build) { |record| record.strict_loading!(false) }
  end

globalに使えるtraitを定義する

globalに使えるtraitを定義する方法は簡単で以下のようにfactoryに紐付けずFactoryBot.define直下にtraitを置くことで定義できます。

FactoryBot.define do
  trait :strict_loaded do
    after(:build) { |record| record.strict_loading!(false) }
  end
end

全然知らなかったのですが、以下の通りテストコードでも検証されているので公式でサポートされている利用方法のようです👀✨

github.com

おわりに

FactoryBot便利ですね🤖✨

参考

stackoverflow.com