Madogiwa Blog

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

Ruby on Rails: GoodJobをRailsアプリケーションと同一プロセスで実行するメモ📝

Heroku等で運用しているとバッチサーバーを別のプロセスで実行するとお金が掛かったりするのでGoodJobを用いている場合にRailsアプリケーションと同一プロセスで起動する方法をメモ📝

GoodJobについてはこちら

madogiwa0124.hatenablog.com

やり方としては簡単で以下のようにgood_job.execution_mode:asyncに変更するだけで大丈夫だった。

 frozen_string_literal: true

Rails.application.configure do
   config.good_job.queues = "*"
-  config.good_job.execution_mode = :external
+  config.good_job.execution_mode = :async
   config.good_job.cleanup_preserved_jobs_before_seconds_ago = 1.hour.to_i
   config.good_job.max_threads = 2
end

以下の通りexecution_mode:asyncに設定するとRailsのアプリケーションプロセス内の別スレッドとして動作させることができるようです。また、consoleやmigration等で起動された場合には動作しないようになっているのもありがたいですね🙏

  • execution_mode (symbol) specifies how and where jobs should be executed. You can also set this with the environment variable GOOD_JOB_EXECUTION_MODE. It can be any one of:
    • :async (or :async_server) executes jobs in separate threads within the Rails web server process (bundle exec rails server). It can be more economical for small workloads because you don’t need a separate machine or environment for running your jobs, but if your web server is under heavy load or your jobs require a lot of resources, you should choose :external instead. When not in the Rails web server, jobs will execute in :external mode to ensure jobs are not executed within rails console, rails db:migrate, rails assets:prepare, etc.

https://github.com/bensheldon/good_job/blob/3933005203f8781d5937e8541256488eee47380f/README.md#configuration-options

コードを軽く読んでみた限りですが、execution_mode:asyncの場合にはRailsアプリケーションが起動されたタイミング(config.after_initialize)でGoodJob._start_async_adaptersが実行され、Railsアプリケーション内にGoodJobの各種機能が有効化され実行が開始され、

    initializer "good_job.start_async" do
      config.after_initialize do
        ActiveSupport.on_load(:active_record) do
          ActiveSupport.on_load(:active_job) do
            GoodJob._framework_ready = true
            GoodJob._start_async_adapters
          end
          GoodJob._start_async_adapters
        end
        GoodJob._start_async_adapters
      end
    end
  end

https://github.com/bensheldon/good_job/blob/3933005203f8781d5937e8541256488eee47380f/lib/good_job/engine.rb#L51-L70

エンキューされたタイミングで別スレッドを作って非同期で処理するようにしているようです。

    def enqueue_at(active_job, timestamp)
      # ...
          executed_locally = execute_async? && @capsule&.create_thread(execution.job_state)
          Notifier.notify(execution.job_state) if !executed_locally && send_notify?(active_job)

https://github.com/bensheldon/good_job/blob/3933005203f8781d5937e8541256488eee47380f/lib/good_job/adapter.rb#L181-L188

※cronとかもどうなるのかなと思ったけどGoodJobの各種機能が有効化されるタイミングでCronのマネージャーも有効化されるからRailsアプリケーションの中でcronも動くようだった。

Solid Queueに移行しようかなと思っていたのですが、

github.com

Solid Queueでは現時点では以下で検討されてますが、まだ同一プロセスでの実行はできないっぽいですね👀

github.com

GoodJob便利!

CSS: `display: flex`で横並びにした要素をモバイルサイズの場合に縦並びにする方法MEMO

display: flexで横並びにした要素をモバイルサイズの場合に縦並びにするときにいい感じのやり方を探して、少しハマったのでメモ📝

結論としては以下の通りモバイルサイズの時だけflex-direction: column;を指定すれば横並びが縦並びになる🤩

.items {
  display: flex;
  justify-content: center;

  @media (max-width: 768px) {
    flex-direction: column;
  }
}

CodePen

See the Pen `display: flex`でモバイルサイズの場合に縦並びにする by madogiwa (@madogiwa0124) on CodePen.

flexbox便利ですねー✍️

developer.mozilla.org

TypeScriptでClassのconstructorの引数の型を取得する方法MEMO

TypeScriptで普通の関数であればParameter<function>で関数の引数の型を取得できますが、Classのconstructorの引数の型を取得する方法がイマイチ分からず少しハマったのでやり方をメモ📝

Parameters
Constructs a tuple type from the types used in the parameters of a function type Type.
https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype

結論としてはConstructorParameters<typeof Class>を使えば取得できた!

ConstructorParameters
Constructs a tuple or array type from the types of a constructor function type.
https://www.typescriptlang.org/docs/handbook/utility-types.html#constructorparameterstype

例えばVue 2からVue 3への移行したいときに、Vue 3に上げる前にnew Vue相当の処理をcreateAppを使って実装できる以下のようなWrapperを用意したりするときに型チェックも行えて便利!!

Vue2Wrapper

import Vue from "vue"; // Vue 2


export const createApp = (...args: ConstructorParameters<typeof Vue>) => {
  return new Vue(...args);
};

entry.ts

import { createApp } from "Vue2Wrapper"; 

const app = createApp({ /* ... */ });

Vue.js: Vueアプリケーション内で発生したエラーをRollbarに通知するメモ📝

個人のWebサービスのエラー通知にRollbarを利用しているのですが、以下の通りデフォルトではVueアプリケーション内で発生したエラー(コンポーネント内のロジックでエラーが起きたケース等)は通知されないことに今更気づき、通知されるようにしたので対応したことをメモ📝

Add Rollbar to Vue’s global error handler. Uncaught exceptions within the Vue app are sent to this handler, and appear in the browser console, but are not sent > to the browser’s global error handler. Adding Rollbar here ensures that these errors are reported. https://docs.rollbar.com/docs/vue-js

Rollbarへ通知するためのプラグインを追加

ほぼドキュメントの通りですが、app.config.errorHandlerを設定し、Vueアプリケーションでエラーが発生した場合にRollbarへの通知ロジックが実行されるようなPluginを用意して、

// NOTE: Rollbarにブラウザ側で発生したエラー通知を送る機能を有効化
// https://docs.rollbar.com/docs/browser-js

import Rollbar from "rollbar";
import type { App } from "vue";

const token = process.env.ROLLBAR_POST_CLIENT_ITEM_ACCESS_TOKEN;
const rollbarParamsValidation = () => !!(!(NODE_ENV === "test") && token);

const rollbar = new Rollbar({
  accessToken: token,
  captureUncaught: true,
  captureUnhandledRejections: true,
  enabled: rollbarParamsValidation(),
  payload: {
    environment: NODE_ENV,
  },
});

export default rollbar;

// NOTE: Vueアプリケーション内部のエラーをRollbarに通知するプラグイン
// https://docs.rollbar.com/docs/vue-js
export const VueRollbarPlugin = {
  install(app: App) {
    app.config.errorHandler = (error, _vm, info) => {
      // NOTE: 公式docでは以下となっていたが、おそらく送信時にvueComponentをjsonに変換するのにブラウザがフリーズするため送信しないようにした。
      // rollbar.error(error, { vueComponent: vm, info });
      rollbar.error(error as Error, { info });
      // NOTE: 本来はエラーをコンソールに出力するのは避けるべきだが、
      // エラー発生時にconsole.errorに表示しておいた方が確認しやすいため残している
      // eslint-disable-next-line no-console
      console.error(error);
    };
    app.provide("rollbar", rollbar);
  },
};

Pluginの有効化

以下のようにcreateApp時にuseして有効化するようにしました。

import NavigationBar from "@js/components/organisms/NavigationBar.vue";
import { VueRollbarPlugin } from "@js/services/Rollbar";
import { createApp } from "vue";

const app = createAppWithDefault({ components: { NavigationBar } });
app.use(VueRollbarPlugin);
app.mount("#vue-header");

また、毎回これを書くのはめんどくさそうに思ったのでcreateAppWithDefaultを定義して、

import { createApp, type CreateAppFunction } from "vue";
import { VueRollbarPlugin } from "./Rollbar";
type VueCreateAppType = CreateAppFunction<Element>;

export const createAppWithDefault: VueCreateAppType = (...args: Parameters<VueCreateAppType>) => {
  const app = createApp(...args);
  app.use(VueRollbarPlugin);
  return app;
};

以下のように利用するとcreateApp後に自動的にuseするようにしました。

import NavigationBar from "@js/components/organisms/NavigationBar.vue";
import { createAppWithDefault } from "@js/services/Vue";

const app = createAppWithDefault({ components: { NavigationBar } });
app.mount("#vue-header");

おまけ:Sentryのケース

Sentryの場合には、@sentry/vueが提供されていてDocumentの通りに設定すると自動的にapp.config.errorHandlerをよしなに設定してくれるっぽかった。

https://docs.sentry.io/platforms/javascript/guides/vue/

Ruby on Rails: capybara-playwright-driverでsystem specを実行するMEMO📝

system specをseleniumではなくcapybara-playwright-driverを使ってplaywrightで動かしてみたのでメモ📝

github.com

playwrightに関しては以下に以前まとめていたので貼り付けておく。

madogiwa0124.hatenablog.com

準備

必要なライブラリをinstallします。

Gemfile

  gem "capybara-playwright-driver"
  gem "playwright-ruby-client"

package.json

    "@playwright/test": "1.41.1",

以下を実行してplaywrightで利用するbrowsersをinstallします。

$ npx playwright install

playwright driverを利用する

RSpec.configuredriven_by:playwrightを指定するだけでplaywrightでsystem specが実行されます🤖

# NOTE: 推奨値に変更
# > It is recommended to set the timeout to 15-30 seconds for Playwright driver.
# > https://playwright-ruby-client.vercel.app/docs/article/guides/rails_integration#update-timeout
Capybara.default_max_wait_time = 15

RSpec.configure do |config|
  config.before(:each, type: :system) do |example|
    example.metadata[:js] ? driven_by(:headless_chrome) : driven_by(:rack_test)
    if example.metadata[:js]
      headless = ENV.fetch("PLAYWIGHT_HEADLESS", "true") == "true"
      browser = ENV.fetch("PLAYWIGHT_BROWSER_TYPE", :chromium).to_sym
      driven_by(:playwright, options: {browser_type: browser, headless: headless})
    else
      driven_by(:rack_test)
    end
  end

スクリーン録画やトレースを利用する

またplaywrightのtraceや録画をcallbackを活用することで取得できるようだったので、

以下のような感じでtmp配下に配置するようにしてみました。

def save_playwright_screen_record(
  example,
  save_dir: "tmp/capybara/screen_records",
  file_name_proc: ->(example) { example.full_description.split(" ").join("_") }
)
  Capybara.current_session.driver.on_save_screenrecord do |video_path|
    file_name = "#{file_name_proc.call(example)}#{Pathname.new(video_path).extname}"
    FileUtils.mkdir_p Rails.root.join(save_dir)
    FileUtils.cp video_path, Rails.root.join(save_dir, file_name)
  end
end

def save_playwright_trace(
  example,
  save_dir: "tmp/capybara/traces",
  file_name_proc: ->(example) { example.full_description.split(" ").join("_") }
)
  Capybara.current_session.driver.on_save_trace do |trace_zip_path|
    file_name = "#{file_name_proc.call(example)}#{Pathname.new(trace_zip_path).extname}"
    FileUtils.mkdir_p Rails.root.join(save_dir)
    FileUtils.cp trace_zip_path, Rails.root.join(save_dir, file_name)
  end
end
RSpec.configure do |config|
  config.before(:each, type: :system) do |example|
    example.metadata[:js] ? driven_by(:headless_chrome) : driven_by(:rack_test)
    if example.metadata[:js]
      headless = ENV.fetch("PLAYWIGHT_HEADLESS", "true") == "true"
      browser = ENV.fetch("PLAYWIGHT_BROWSER_TYPE", :chromium).to_sym
      driven_by(:playwright, options: {browser_type: browser, headless: headless})
+    save_playwright_trace(example) if example.metadata[:trace]
+    save_playwright_screen_record(example) if example.metadata[:screen_record]
    else
      driven_by(:rack_test)
    end
  end

ビデオの保存はdriven_by(:playwright, options: {browser_type: browser, headless: headless, record_video_dir: "tmp/capybara/screen_record"})のようにrecord_video_dirを指定するだけも任意のディレクトリに配置できるがファイル名をいい感じにしたかったのでゴリっと書いてみた🦍 (もっといいやり方ありそうだけど。。。)

we can record the videos without specifying record_video_dir explicitly or preparing a temporary directory. capybara-playwright-driver automatically prepare and set record_video_dir internally. https://playwright-ruby-client.vercel.app/docs/article/guides/recording_video#using-screen-recording-from-capybara-driver

おわりに

capybara-playwright-driver 簡単に乗り換えられてありがたい・・・!!🙏✨

参考

note.com

個人のWebサービスをVite 5.0系にアップデートしたので対応したことMEMO📝

2023/11/16 Vite v5がリリースされました⚡️

vitejs.dev

リリースから2ヶ月経ちライブラリ側のサポートも揃ってきたので、 個人のWebサービスをVite 4系からVite v5系にアップデートしたので対応したこととかをメモしておきます📝

前提事項

利用しているライブラリは以下のような感じです。(Ruby on Railsのサービスでフロントエンド関連のファイルのビルドにViteを利用しています。)

Vite v5アップデートで対応したこと

以下のドキュメントに公式のマイグレーションガイドがあるので読みつつ進めました。

以下に具体的に必要だった対応事項を記載します。

Vite v5 からCSSファイルはトップレベルに含まれなくなったのでstylesheet_pack_tagの利用をやめる

以下の通りVite v5 からCSSファイルはminifest.jsonのトップレベルに含まれなくなりました。

対応する CSS ファイルは manifest.json ファイルのトップレベル項目としてリストされない Vite 4 では、JavaScript エントリーポイントに対応する CSS ファイルもマニフェストファイル (build.manifest) のトップレベルエントリーとしてリストされていました。 これらのエントリーは意図せずに追加されたもので、単純な場合にのみ機能しました。 https://ja.vitejs.dev/guide/migration.html#%E5%AF%BE%E5%BF%9C%E3%81%99%E3%82%8B-css-%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AF-manifest-json-%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%83%88%E3%83%83%E3%83%95%E3%82%9A%E3%83%AC%E3%83%98%E3%82%99%E3%83%AB%E9%A0%85%E7%9B%AE%E3%81%A8%E3%81%97%E3%81%A6%E3%83%AA%E3%82%B9%E3%83%88%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84

元々は以下のような感じでトップレベルのCSSを直接参照していたのですが上記の破壊的変更によりよみこめなくなりエラーとなっていました。

<%= vite_stylesheet_tag 'application' %>

読み込めなくなっていたもののvite_javascript_tagの以下の処理でentryのjs配下のcssstylesheet_link_tagで挿入されるため、

  def vite_javascript_tag(*names, # 省略
    # 省略
    tags << stylesheet_link_tag(*entries.fetch(:stylesheets), media: media, **options) unless skip_style_tags

    tags
  end

ref: https://github.com/ElMassimo/vite_ruby/blob/369facf440f41162efee825a87d9491ff83a03b8/vite_rails/lib/vite_rails/tag_helpers.rb#L52

stylesheet_pack_tagの利用自体が不要だったので削除しました。

- <%= vite_stylesheet_tag 'application' %>

Vite v5からcjsが非推奨になったのでpackge.jsonのtypeをmoduleに変更する

以下の通りVite v5から CJS Node API の非推奨化され警告が出ていたので、

CJS Node API の非推奨化 Vite の CJS Node API は非推奨になりました。 今後、require('vite') を呼ぶときは、非推奨の警告メッセージが出力されます。 ファイルやフレームワークを更新して、代わりに Vite の ESM ビルドをインポートするとよいでしょう。 https://ja.vitejs.dev/guide/migration.html#cjs-node-api-%E3%81%AE%E9%9D%9E%E6%8E%A8%E5%A5%A8%E5%8C%96

The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.

以下のようにcjsの記法をESM形式に修正する or ファイルの拡張子を.cjsに修正し、

- const autoprefixer = require("autoprefixer");
+ import autoprefixer from 'autoprefixer';
+ import tailwindcss from 'tailwindcss';

- module.exports = {
-   plugins: [require("tailwindcss"), autoprefixer({ grid: true })],
+ export default {
+   plugins: [tailwindcss, autoprefixer({ grid: true })],
};

package.jsontypemoduleに変更しました。

+ "type": "module",

historeによるコンポーネントカタログのbuild時に、まだ警告が出ていますがhistoire側の問題なので対応していません

github.com

おわりに

基本的にVite公式のマイグレーションガイドに従ってバージョンアップできました🙌 日本語訳もあり非常にありがたかったです・・・!

Git管理されたプロジェクト内の配下のTypeScriptプロジェクトだけ型チェックを行うスクリプトメモ📝

Git管理されたプロジェクトにいくつかTypeScriptのプロジェクトがあり、それらをまとめて型チェックしたくスクリプトを書いたのでメモ📝

以下がそのスクリプトです。

git ls-files -- root/dir | \
grep "tsconfig.json" | sed 's/tsconfig.json//' | \
xargs -I {} sh -c "cd {}; echo \"\n\"; pwd; npm install; npm run tsc --noEmit"

git ls-files -- root/dirで特定ディレクトリ配下のGit管理下のファイルリストを出力し、 grep "tsconfig.json" | sed 's/tsconfig.json//'tsconfig.jsonが配置されているディレクトリリストを取得します。

その後、xargs -I {} sh -c "cd {}; echo \"\n\"; pwd; npm install; npm run tsc --noEmit"でそれぞれのディレクトリに移動し、npm install後にtscで型チェックします。 ※上述のスクリプトでは対象が分かりやすいようにpwdで現在のディレクトリも表示しています。

ちなみにxargsで実行された複数のスクリプトの一部が失敗した場合、exit codeは123になるようです。 GitHub Actionsはexit codeが0以外は失敗扱いになるから、xargsで成功と失敗が混在した場合には失敗になるので便利ですね!

docs.github.com

参考

yujiorama.hatenablog.com

git-scm.com