Madogiwa Blog

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

Ruby on Rails: 今後標準で使えるようになるかもしれないViewComponentを試してみる

ViewComponentとは?

3rd Party製のコンポーネントフレームワークのためにレンダー済みのHTMLを返すrender_inが定義されているObjectをActionView:: Helpers::RenderingHelper#renderに渡せるようになりました。

github.com

上記対応は当初ViewComponentの前準備としてあげられたPRだったので、Rails 6.1で標準になるのでは?と期待があったのですが、

techracho.bpsinc.jp

特に入らず現状はGitHub社がViewComponentというgemとして公開しています。

github.com

ViewComponentの使い方

Gemfileに以下を追加して、bundle install を実行します。

gem "view_component", require: "view_component/engine"

以下のコマンドでViewComponentで利用するファイルを作成出来ます。

$ bin/rails generate component Example title

主な生成ファイルと主な用途を以下に記載します。

  • app/components/example_component.rb : ロジック、引数をもとにインスタンス変数を生成
  • app/components/example_component.html.erb : テンプレート、ロジックで生成したインスタンス変数を用いた描画部分を担当

サンプル

app/components/example_component.rb

class ExampleComponent < ViewComponent::Base
  def initialize(title:)
    @title = title
  end
end

app/components/example_component.html.erb

<span title="<%= @title %>"><%= @title %></span>

またcallメソッドにインラインでテンプレートを定義することも出来ます。

class ExampleComponent < ViewComponent::Base
  def initialize(title:)
    @title = title
  end

  def call
    ERB.new('<span title="<%= @title %>"><%= @title %></span>').result(binding)
  end
end

※inlineとテンプレートファイルを併用すると、ViewComponent::TemplateErrorになるので併用は出来ません。

ViewComponentをテストする

公式ドキュメントはこちら

viewcomponent.org

ViewComponentはbin/rails generate component時にconfig.generators .test_frameworkを参照し、適切なテストファイルを生成してくれます。

今回はRSpecでテストを実行するケースで実行方法をメモしておきます。

以下に記載されているとおりですが、rails_helperに以下を追記するだけでテストの準備はOKです🙆‍♂️

require "view_component/test_helpers"

RSpec.configure do |config|
  config.include ViewComponent::TestHelpers, type: :component
end

あとは以下のような形でレンダリング結果をテスト出来ます✨

require "rails_helper"

RSpec.describe ExampleComponent, type: :component do
  it "renders component" do
    result = render_inline(ExampleComponent.new(title: "my title"))
    expect(result.to_html).to eq <<~HTML
        <span title="my title">my title</span>
    HTML
  end
end

おまけ:VueのSFCみたいなのは実現できるか?

個人的には、結論から言うと一部は出来そうですが、厳しそうな感じかなと。。。

HTML5.2からbodyタグの中にstyleタグが記載できるようになったので、VueComponent内で指定したスラグをCSSセレクタとテンプレートのTOPレベルの要素のclassに指定すれば擬似的にScoped CSSみたいなのが実現出来るかなぁとか妄想したのですが、

vanillaice000.blog.fc2.com

実際にコードに起こすと以下のような感じになったのですが、

  • inlineでCSSを書いてしまうと、stylelint、autoprefixer等の既存CSS周りのツール群が使えない
  • エディターの自動補完が機能しない

と結構厳しい感じにですね、、、webpackにvue-component-loaderみたいなのを作ってcssをbuild時に生成するみたいなことをやると、autoprefixerを通すとかはいけたりするんですかね?

# frozen_string_literal: true

class ExampleComponent < ViewComponent::Base
  def initialize(title:, slug:)
    @title = title
    @slug = slug.presence || build_slug
  end

  def call
  ERB.new(style+template).result
  end

  private

  attr_reader :title, :slug

  def template
    <<~ERB
      <div class="<%= @slug %>">
        <h1><%= @title %></h1>
      </div>
      ERB
  end

  def style
    <<~ERB
      <style>
      .<%= @slug %> h1 {
          font-size: 50px;
        }
      </style>
      ERB
  end

  def build_slug
    SecureRandom.urlsafe_base64(8)
  end
end

おわりに

ViewComponent、さすがのGitHub社製ということもあり、configの値をちゃんと見れくれたりとかゆいところに手が届いてる感じが良いですね✨

公式ドキュメントには、他にもいろいろな機能が記載されているのでぜひ気になる方は見てみてください📚

viewcomponent.org

Vue3系×Webpack5系の環境にStorybookを導入するMEMO📝

以下のような環境にStorybookを導入してみたので対応したこととかをメモしておきます。

package version
vue 3.1.4
webpack 5.44.0

Storybookとは

まずはStorybookについて簡単に説明します。

storybook.js.org

Storybook is an open source tool for building UI components and pages in isolation. It streamlines UI development, testing, and documentation.

上記の通りUIコンポーネントの開発ツールです、以下のような形でコンポーネントのサンプルを閲覧したり、私がVueを使っているのですが、propsをGUI上で設定して動作確認が行えたりします。

f:id:madogiwa0124:20210717163859p:plain

Storybookを導入する

今回はStorybookをVue3、Webpack5の環境に導入する前提で導入方法をメモしていきます。

Storybookのinstall

まずは以下のコマンドを実行してWebpack5用のStorybookをinstallします。

$ npx sb init --builder webpack5

上記コマンドを実行すると以下のようなことが実行されます。

  • Storybook関連ライブラリのインストール
  • Storybook用の設定ファイル群の生成
  • サンプルファイルの生成

Vue3を使用していたので特別な設定が必要かなと思ったのですが、init時に自動的に判断してVue3用の設定をしてくれるようです、便利✨

既存のWebpackと設定をあわせる

実際に自身でWebpackの環境をすでに用意している場合にはalias等の設定をStorybook用のWebpackと合わせておかないとmoduleの解決ができないので.storybook/webpack.config.jsを用意して上げて修正します。

const path = require("path");
const rootPath = path.resolve(__dirname, "../");

module.exports = ({ config }) => {
  config.resolve.alias["@"] = rootPath;
  config.resolve.alias["~"] = rootPath;
  config.resolve.alias["@js"] = `${rootPath}/app/javascript`;
  config.resolve.alias["@css"] = `${rootPath}/app/javascript/stylesheets`;
  config.resolve.alias["@spec"] = `${rootPath}/spec/javascript`;
  config.resolve.alias["querystring"] = "querystring-es3";
  config.resolve.alias["process"] = "process/browser";

  config.module.rules.push({
    test: /\.(scss|css)/,
    use: ["style-loader", "css-loader", "sass-loader"],
  });

  return config;
};

私の場合はaliasの設定に加えて、VueのSFCのstyle内のでimportが失敗してたのでcssまわりの設定も既存のWebpackから移植しました。

tacamy.hatenablog.com

グローバルで読み込ませたいファイルのimport

またcssフレームワーク等は、グローバルに読み込ませたかったのでpreview.jsでimportするようにしました。

// 全体に適用するCSSをimport
import "@js/stylesheets/application.scss";

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

qiita.com

Storyを配置するディレクトリを設定

.storybook/main.jsstoriesに自分がStoryを配置したいディレクトリパスを設定してあげます。

module.exports = {
  stories: ["../app/javascript/components/**/*.stories.@(js|jsx|ts|tsx)"],
  addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
  core: {
    builder: "webpack5",
  },
};

Storyを追加する

あとは以下のような感じで任意のコンポーネントのStoryを追加してあげればOKです🎉

import FeedCard from "@js/components/feed/FeedCard.vue";
import { buildFeed } from "@spec/mocks/Feed";

export default {
  title: "FeedCard",
  component: FeedCard,
};

const Template = (args) => ({
  components: { FeedCard },
  setup() {
    return { args };
  },
  template: '<feed-card  v-bind="args" />',
});

const feed = buildFeed(1);
feed.lastEntry.eyeCatchingImage = "";

export const Default = Template.bind({});
Default.args = {
  feed: feed,
};

この辺の記載方法は以下を参照してください📚

storybook.js.org

MEMO

以下はメモ

node_modules配下のTypeScriptの型チェックで落ちる

なぜか、node_modules配下のTypeScriptの型チェックで落ちるようになったので、

node_modules/@vue/runtime-dom/dist/runtime-dom.d.ts:1430:15 - error TS2320: Interface 'IntrinsicAttributes' cannot simultaneously extend types 'ReservedProps' and 'Attributes'.
  Named property 'key' of types 'ReservedProps' and 'Attributes' are not identical.

以下のようにskipLibCheckをtrueにしました。

"skipLibCheck": true

stackoverflow.com

おわりに

Storybook今まで使ったことがなかったのですが、GUIでpropsをいじって動作確認出来たり、マージンのpx等も確認出来たりと動作確認に便利ですね!

また静的に出力もできるのでgithub pagesとかにhostingしてコンポーネント設計のドキュメントとしてそのまま利用できそうなところも、良さそう・・・!

個人のWebサービスをVue2からVue3系へのアップグレードしてみたのでMEMO

個人のWebサービスをVue2系からVue3系にアップグレードしてみたので、やったこととかメモしておきます📝

基本的な流れとしては以下の記事を参考にさせていただきました🙇‍♂️

zenn.dev

事前準備

plugin:vue/vue3-recommendedを有効化する

まずはeslint-plugin-vuevue3-recommendedを有効化して静的解析を行うとVue3で非推奨になったオプション等に警告を出してくれるので、以下の通り有効化して対応を行いました👮

  "extends": [
    "eslint:recommended",
    "plugin:vue/vue3-recommended",

Vue本体のアップデート

ライブラリの更新・不要なライブラリの削除

以下のような形で関連ライブラリの更新を行いました。

yarn add vue-jest@next @vue/test-utils@next @vue/compiler-sfc vue-jest@next --dev
yarn add vue@next

またVueの更新により不要となった以下のライブラリはpackage.jsonから削除しました。

  • vue-template-compiler
  • @vue/composition-api

型定義の更新

参考記事の通り以下のようにVueの型定義を更新しました。

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

createAppを使ってVueインスタンスを作成

以下のような形でnew VueではなくcreateAppを使ったVueインスタンスの生成方法に変更しました。

// before
import Vue from "vue";
import IndexBoardContainer from "@js/components/containers/IndexBoardContainer.vue";

new Vue({
  el: "#vue-root",
  components: { IndexBoardContainer },
});

// After
import { createApp } from "vue";
import IndexBoardContainer from "@js/components/containers/IndexBoardContainer.vue";

const app = createApp(IndexBoardContainer);
app.mount("#vue-root");

Componentの書き換え

基本的には記事通りで以下のようにコンポーネントを書き換えていきます。

<script lang="ts">
import { defineComponent } from 'vue';

export default defineComponent({})
</script>

Composion APIに書き換えなくてもOptionsAPIの場合でも以下のように書き換えることで使用出来るようになりました。

// before
import Vue from "vue";
export default Vue.extend({})

// after
import { defineComponent } from "vue";
export default defineComponent({})

関連ライブラリまわりの対応

Webpack関係

vue-loader

requireの処理を以下のように修正しました。

const { VueLoaderPlugin } = require("vue-loader");

qiita.com

設定の修正

aliasの削除

以下のaliasを削除、多分buildしたものを参照しなくても良くなっている。

    alias: {
      vue: "vue/dist/vue.esm.js",
2021/07/18 追記 互換性を維持するには消しちゃだめだった

完全ビルドが必要な場合はaliasを削除するのではなく以下に書き換える必要あり

    alias: {
      vue: "vue/dist/vue.esm-bundler.js",

blog.capilano-fw.com

警告You are running the esm-bundler build of Vueの抑制

以下の警告が発生するようになったので

VM373 runtime-core.esm-bundler.js:4615 You are running the esm-bundler build of Vue. It is recommended to configure your bundler to explicitly replace feature flag globals with boolean literals to get proper tree-shaking in the final bundle. See http://link.vuejs.org/feature-flags for more details.

以下を参考にしてDefinePluginを使って値を設定するようにしました。

  plugins: [
    new webpack.DefinePlugin({
      __VUE_OPTIONS_API__: true,
      __VUE_PROD_DEVTOOLS__: false,
    }),

stackoverflow.com

設定値の詳細はこちら

https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags

vue-fontawesome

Vue3にアップグレード後に以下の警告が出るようになりました。

VM2681 runtime-core.esm-bundler.js:150 [Vue warn]: Failed to resolve component: font-awesome-icon
  at <FeedCard feed=

公式ガイドにしたがって@prereleaseを使うようにしました

github.com

vue-infinite-loading

I've published an pre-alpha version vue-infinite-loading@3.0.0-alpha.0-0 base on the PR branch, as the temporary way for Vue.js 3.0 projects.

vue-infinite-loading@3.0.0-alpha.0-0が公開されているとのことで、以下のコマンドでinstall。一旦動いてそうな感じだったので、こちらをそのまま使うことにしました。

yarn add vue-infinite-loading@3.0.0-alpha.0-0

github.com

ハマった事象と対応したこと

なぜかcreateAppにComponentを指定してもレンダリングされない。

// エラーにらなずにレンダリングが走らない。
// const app = createApp({
//   components: { NewBoardContainer },
// });

// こっちだと行ける
const app = createApp(NewBoardContainer);

// 以下のFontAwesomeIconは適用されているのでRootComponentを空にしてるとだめっぽい?
app.component("FontAwesomeIcon", FontAwesomeIcon);
app.mount("#vue-root");

とりあえずentryからレンダリングするcomponentは全部RootComponentにするようにmountまわりを調整して対応しました。。。

2021/07/18 追記

「2021/07/18 追記 互換性を維持するには消しちゃだめだった」に記載した件が原因、ランタイムビルドがデフォルトで読み込まれてしまうので、完全ビルドをimportする必要があった。

参考:vue/dist/vue.esm.js って何~【とりあえず動くからいいや】からの卒業~ - Qiita

RootComponentにhtmlからpropsが渡せない。

const app = createApp(ShowFeedContainer);
app.component("FontAwesomeIcon", FontAwesomeIcon);
app.mount("#vue-root");
<div id="vue-root">
  <!-- component側だとfeedIdがundifinedになる -->
  <show-feed-container :feed-id="1">
</div>

解決できなかったので、ComponentのcreatedでPropsで取得していた値をAPI経由で渡すようにして対応しました。。。

2021/07/18 追記

おそらく「2021/07/18 追記 互換性を維持するには消しちゃだめだった」に記載した件が原因、ランタイムビルドがデフォルトで読み込まれてしまうので、完全ビルドをimportする必要があった。

参考:vue/dist/vue.esm.js って何~【とりあえず動くからいいや】からの卒業~ - Qiita

TypeError: babelJest.getCacheKey is not a functionが発生してjest実行時に落ちる。

https://twitter.com/TheJaredWilcurt/status/1406812612600422400

babel-jestjestのバージョンをあわせたら治りました。

  "babel-jest": "^26.6.3",
  "jest": "^26.6.3",

TypeError: moment_1.default is not a functionが発生してjest実行時に落ちる

    TypeError: moment_1.default is not a function

      101 |     },
      102 |     lastPublishedAtText: function (): string {
    > 103 |       return moment(this.feed.lastEntry.publishedAt).format("YYYY/MM/DD h:mm:ss");
          |              ^
      104 |     },
      105 |     lastPublishedAtFromNow: function (): string {
      106 |       return moment(this.feed.lastEntry.publishedAt).fromNow();

Component側でimport * as moment from "moment"するようにしたら治りました。

https://github.com/aurelia/skeleton-navigation/issues/606#issuecomment-232802977

まとめ

Vue2からVue3は結構破壊的な変更が多そうなのと、まだ対応できていない関連ライブラリ等も多そうな印象に見えたのので、まだハマりどころが多そうな印象ですが、参考になりましたら幸いです…!

本当に見られたくない情報はレスポンスの検証もしたほうが良いというMEMO

タイトル通り、本当に見られたくない無い要素はrequest specでも検証したほうが良さそうに思ったので、自戒の意味も込めてメモしておきます📝

想定しているケース

以下のような要件があり、

  • ログインユーザーにはすべての情報を表示
  • 未ログインユーザーには一部の情報だけ表示

以下のような実装をしていたとします。

  • 表示部分は、フロントエンドで動的に表示
  • ログインと未ログインでは共通のインターフェイスでやりとりし、JSONをバックエンドからフロントエンドに渡す
  • フロントエンドでユーザーのログイン状態を判定し未ログインの場合は一部の情報を非表示する

実装イメージ

RailsとVueの場合を想定

def index
  @posts = Post.all
  @logged_in = user_logged_in?
end
<posts :init-posts=<%= @posts.to_json %> :logged_in=<%= @logged_in %> />

問題点

ログインと未ログインでは共通のインターフェイスでやりとりし、JSONをバックエンドからフロントエンドに渡す

共通のインターフェイスでやりとりし、JSONをバックエンドからフロントエンドに渡しているのでバックエンドからのレスポンスを未ログインユーザーが見た場合、JSONの中身が見えてしまうので本来だったら見せたくない情報が閲覧出来る状態になってしまっています。

レスポンスイメージ

<posts :init-posts="[{title: "title", secret: "秘密の情報" }]" :logged_in=false />

しかもブラウザによるDOMのレンダリング後は非表示になるため、system spec等で普通にE2Eテストを書いてるだけでは検知できません。。。

対応策

見せたくない情報はマスキングして渡す

以下のようにそもそも未ログインの場合は不要な情報のハズなのでマスキングして渡す事によってレスポンスを見てもマスキングされた値が入っているので閲覧されることはありません。

MASKED_TEXT = "******"

def index
  @posts = Post.all.map { |post| { title: post.title, secret: MASKED_TEXT } } 
  @logged_in = user_logged_in?
end

レスポンスイメージ

<posts :init-posts="[{title: "title", secret: "******" }]" :logged_in=false />

レスポンスを直接検証して発生を未然に防ぐ

E2Eテストだと検知が難しいので直接レスポンスボディを検証するようなテストを行うことで、CI等で発生を検知しリリース前に気づくことができます。

let!(:post) { create(:post, secret: "秘密の情報") }

before { get posts_path }

it '見えちゃいけない情報が見えないこと' do
  expect(response.body).not_to include post.secret
end

おわりに

一見すると見えていない部分でも意図せず情報を公開してしまうようなことが発生するとクリティカルだったりするので、見えちゃいけない情報はそもそもレスポンスとしても返さないように気をつけたいですね・・・!

OK Computerのヘルスチェックをブラウザで見やすくするGemを作りました💎

OK Computerのヘルスチェックをブラウザで見やすくするGemを作ったので、使い方とかをメモしておきます📝

作ったGem

github.com

モチベーション

github.com

OK Computerはヘルスチェックをしてくれる便利なGemなのですが、Viewがテキスト or JSONのみの用意となっており、ブラウザで確認したときの表示が以下のようにちょっと成功しているか等がパット見でわかりにくかったので、

f:id:madogiwa0124:20210626155506p:plain

HTML用のViewを作成して以下のような形で見やすくしたかったのがモチベーションです👩‍🎨

f:id:madogiwa0124:20210626155533p:plain

f:id:madogiwa0124:20210626163741p:plain

使い方

GitHubリポジトリを指定してbundle installをして、

git_source(:github) { |repo| "https://github.com/#{repo}.git" }

gem 'okcomputer_html_view', github: 'Madogiwa0124/okcomputer_html_view'

以下のようにconfig/application.rbload OkcomputerHtmlView.load_pathを実行するだけです。

module YourApplication
  class Application < Rails::Application
    config.to_prepare do
      load OkcomputerHtmlView.load_path
    end
  end
end

OK Computerをmountしたパスにアクセスすると以下のような感じで表示されるはずです!

f:id:madogiwa0124:20210626155533p:plain

仕組み

OK ComputerのViewはRails engineで作られているので、以下のRails Guideを参考に

railsguides.jp

Controllerをオーバーライドしてhtml用のrespoonseを定義して、

OkComputer::OkComputerController.class_eval do
  self.view_paths = "#{::OkcomputerHtmlView.gem_dir}/app/views/ok_computer"

  def respond(data, status)
    respond_to do |format|
      format.text { render plain: data, status: status, content_type: "text/plain" }
      format.html { render "/#{action_name}", locals: {data: data, status: status}, content_type: "text/html" }
      format.json { render json: data, status: status }
    end
  end
end

あとはself.view_pathsで指定されたディレクトリにhtml.erbを用意してあげた感じです。

またGemのinstallパスは固定値で書いてしまうと参照できなくなってしまうため、Gem::Specification.find_by_name("okcomputer_html_view").gem_dirを使用してinstlalパスを取得して、それをGem内のコードで使用するようにしています。

# frozen_string_literal: true

require_relative "okcomputer_html_view/version"

module OkcomputerHtmlView
  def self.load_path
    "#{gem_dir}/app/controllers/ok_computer/ok_computer_controller.rb"
  end

  def self.gem_dir
    Gem::Specification.find_by_name("okcomputer_html_view").gem_dir
  end
end

おわりに

Rails engineまわり、あまり触ったことなかったのですが意外と拡張もしやすくて良いですね✨

参考

stackoverflow.com

FactoryBotのinitialize_withを使って初期化時の挙動をfind_or_initialize等に差し替える方法MEMO

一意制約をかけつつ一定の範囲内にある値を扱いたいときにFactoryBotの単純なsequenceでは対応できなくて、いい感じのやり方無いかなと思って調べていたらinitialize_withを使うといい感じにfind_or_initializeに差し替えて一意制約を回避しつつ範囲内の値を生成できたので使い方をメモしておきます。

github.com

「やりたいこと」と「問題点」

例えば以下のような一意のステータスを管理するモデルがあったとします。

class Master::Status
   validates :name, :inclusion: { in: %w(draft wait approved) }, uniqueness: true   
end

単純にFactoryを作ってしまうと以下のような形になります。

FactoryBot.define do
  factory :master_status do
    name { 'draft' }
  end
end

しかし上記のFactoryでは以下のような問題があり使いにくいです 😢

  • 単純にcreate(:master_status)をして行くと一意制約に引っかかりエラーが発生してしまう
  • 上記のエラーを回避するためにFactoryの生成前に存在チェック等の処理をいちいち行わないと行けない

initialize_withを使った解決

そんなときにinitialize_withを使って生成処理をfind_or_initialize_byに差し替えて上げると便利です✨

FactoryBot.define do
  factory :master_status do
    initialize_with { Master::Status.find_or_initialize_by(name: name) }

    name { 'draft' }
  end
end

find_or_initialize_byに差し替わったことにより、DBに値が入っている場合にはfindされ、なければnewされるので、DBの状態を気にせずMaster::StatusをFactoryBotで生成することが出来るようになりました🙌

おわりに

FactoryBot、あんまりちゃんとドキュメント読んでなかったのですが、読んでみると色々いい感じの機能がありますね🤖

参考

stackoverflow.com

個人のWebサービスにGA(Google Analytics)を導入して色々トラッキング出来るようにしたMEMO📝

個人のWebサービスでGAでトラッキング出来るようにしてみたので、やったこととか手順とか色々、MEMOしておきます📝

GA(Google Analytics)とは?

marketingplatform.google.com

ユーザー像を把握。 ユーザー像を詳しく分析して理解を深めましょう。Google アナリティクスなら、ビジネスのデータ分析に必要なさまざまなツールを無料でご利用いただけます。

上記の通りGoogle製の無料で使えるユーザートラッキングのためサービスです。

リアルタイムに現在のユーザー数や発生しているイベントを見たり、レポートを作成したりして、サイトを分析することができます。

GAを導入する

Google Analyticsを導入するのは簡単で私は以下を参考に作成して、

appweb-ga.com

表示されたタグを任意のサイトのheadに配置すればOKです。

私はRuby on Rails製のサービスに導入したので以下のような形で反映しました。

<head>
  <!-- Global site tag (gtag.js) - Google Analytics -->
  <% ga_id = Rails.configuration.settings.google_analytics[:id] %>
  <script async src="<%= "https://www.googletagmanager.com/gtag/js?id=#{ga_id}" %>"></script>
  <script>
    // entryからscriptタグで読み込む方法だとGlobalでGtagが参照できない ので、直接htmlに記載してる。
    window.dataLayer = window.dataLayer || [];
    function gtag() {
      dataLayer.push(arguments);
    }
    gtag("js", new Date());

    gtag("config", "<%= ga_id %>");
  </script>
</head>

一応、GAのIDに関してはconfig_for環境変数を使ってコード内に記述しないようにしています。

production:
  google_analytics:
    id: <%= ENV['GOOLE_ANALYTICS_ID'] %>
development:
  google_analytics:
    id: <%= ENV['GOOLE_ANALYTICS_ID'] %>
test:
  google_analytics:
    id: <%= ENV['GOOLE_ANALYTICS_ID'] %>

GAで任意のイベントをトラッキングする

GAで任意のイベントをトラッキングするには「GAを導入する でGlobalに定義したgtagを呼び出せばOKです🙆‍♂️

以下のような形でGlobalに定義されたgtagを任意の引数で呼び出せるmoduleを用意してあげて、 ※失敗したときに備えて個人で利用しているRollbarに失敗を通知するようにしています。

import Rollbar from "@js/services/Rollbar";

export const trackEvent = ({action, category, label}) => {
  try {
    window.gtag("event", action, {
      event_category: category,
      event_label: label,
    });
  } catch (error) {
    Rollbar.warning("window.gtag is not a function", error);
  }
};

以下のような形で呼び出して上げるとGA側にイベントが反映されます。

import { trackEvent } from "@js/services/Gtag";
trackEvent({
  action: "click_foo",
  category: location.pathname,
  label: "targat name",
});

※上記はどこで(category)、何が(label)、行われた(action)みたいな形でトラッキングするような形にしてみました。

f:id:madogiwa0124:20210612213733p:plain ※上の画像はイメージなのでサンプルのイベントは記載されてないですが。。。

おわりに

あんまり今までGAとかちゃんと使ってこなかったのですが、任意のイベントを手軽にトラッキングして、分析出来るの便利ですね✨

参考

note.com

blog.hubspot.jp