Rails

認証が必要なページにAdsenseを貼る場合の注意点

認証が必要なページにGoogle Adsenseを貼り付けた際に発生した問題に対応した備忘録。わりとハマってしまったので書き留めておく。

その問題とは?

認証エラーの例外が山ほど発生するというもの。

状況としては、次のようなWebアプリだった。

  • Rails製Webアプリ
  • 例外が発生するとSlackで通知する(slack_notifier + exception_notification
  • 認証管理はdevise + cancancan
  • ログインしていないと遷移できないページがあり、未ログインでアクセスすると例外AccessDeniedが発生する
  • それらのページ内にAdsenseの広告あり

で、ログイン済の状態でこれらのページにアクセスすると、アプリ自体は予期した通りに動くのだが、Slackに例外が上がってくる。アクセスする度に上がってくるので非常に鬱陶しい。

もちろんこれは私がSlackを使ってなくても、ログには同じ例外が吐き出されているので誰にでも起こりうる話。

Slackで確認

はじめにこの例外を見たとき、一体自分以外に誰がアクセスしてくるんだ?と思いSlackを確認したが、slack_notifierのデフォルト設定ではUser-agentの値は通知してくれない模様。そこで、application_controller.rbを次のように変更した。

📄app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
...
  before_action :set_notification
...
  private

  def set_notification
    request.env['exception_notifier.exception_data'] = {
      http_user_agent: request.env['HTTP_USER_AGENT']
    }
  end
...
end

そしてページにアクセスして例外を発生させる。Slackを見てみると、次のようにDataが追加されている。

Mediapartners-Googleとな? これを早速ググってみると、どうやらGoogle Adsense用のクローラーであり、ページに最適な広告を選定するため、Adsenseが貼られているページにアクセスしてくるらしい。詳しくはコチラ

解決方法

こいつがAccessDenied例外を発生させている原因。Googleのヘルプを読むとrobots.txtでクロールを禁止できるが、禁止すると広告は表示されなくなるらしい。今回の要件としては別に広告を表示する必要もなかったので、robots.txtを編集してみる。・・・がそれでもクロールが止まらない!

その後Googleのヘルプをよくよく読むと、User-agent: *では、Mediapartners-Googleのクロールは止められないらしい。ちゃんとUser-agent: Mediapartners-Googleと指定しなさいと書いてあるじゃないか・・・。

といわけで、次のようにrobots.txtを変更したら、クロールが止まった。

📄public/robots.txt
User-agent: *
Disallow: /admin/

User-agent: Mediapartners-Google
Disallow: /admin/

ふー。ハマっちまったぜ。。。ま、普通は管理ページに広告貼る人はあまりいないと思うので、わりとレアなケースだったのかもしれない。 ...続きを読む

認証が必要なページにAdsenseを貼る場合の注意点 Read More »

【factory_girl】子のファクトリ生成時に親の属性をまとめて設定する

タイトル読んでも「ちょっと何言ってるか分かんない」感じだが、要するにhas_oneやhas_manyで親子関係を持つモデルがあったとして、子のファクトリ生成時に親側の属性も設定するという話(その逆もしかり)。

たとえばUserクラスとConfigクラスがあったとして、それぞれ次のような関連を持ってるとする。

📄spec/factories/config.rb
class User < ApplicationRecord
  has_one :config
end

class Config < ApplicationRecord
  belongs_to :user
end

各ユーザーが各々設定を持つというシチュエーションを想定している。で、この場合に親のファクトリをcreate(:config)のような感じで作るときに、まとめてUser側の属性も設定したいということ。

通常の書き方だと、Configのインスタンスを得るにはこんな感じになるはず。(たぶん)

config = create(:user. name: '...', email: '...',
  config: build(:config, attr1: '...', attr2: '...')).config

それを次のように書けるようにする。

config = create(:config, attr1: '...', attr2: '...',
  user: { name: '...', email: '...' })

「なんだ、ほとんど変わんねえじゃねーか」とお思いになるかもしれない。しかし前者は、Configのインスタンスが欲しいんだということがひと目で分からない。また、build :config してるのもいささか冗長だ。後者の方がテストの関心がConfigにあることが一目瞭然だし、タイプ量も少なくてすむ。細かいことだけど、こういうのって結構大事だと思うんだな。

ファクトリ定義

で、肝心のファクトリの定義だが、私はこんな感じに定義している。

FactoryGirl.define do
  factory :config do
    attr1 '...'
    attr2 '...'

    transient do
      user { {} }
    end

    after(:build) do |config, evaluator|
      config.user = build(:user, evaluator.user) if evaluator.user.present?
    end
  end
end

userをtransientにしておきuserの属性(Hash)を渡してもらう。after(:build)の中でuserのファクトリに横流してbuildしているのでuserのファクトリ定義を上書きすることができるというわけ。なお親から子の属性を設定する場合も同様の方法が使えると思う。

これがベストの解かは分からない。もっといい方法があれば教えて欲しいッス。

【factory_girl】子のファクトリ生成時に親の属性をまとめて設定する Read More »

【Rails】リロード時に特定の処理を実行する

railsアプリを開発していると、自作DSLだとか設定ファイルを読み込んで何らかの処理を実行するということがよくある。その際に問題になるのは、設定ファイルを変更するたびにいちいちサーバを再起動しないといけないということ。小さなアプリならまだしも、そこそこの規模になってくると、再起動にも結構時間がかかてしまい、鬱陶しいことこの上ない。そこで特定のファイルを監視しておき、更新されていたら次回リクエスト時に特定の処理を走らせることにする。

環境

  • ruby on rails 4, 5

For Rails 5

まずは rails 5 のコードから。次のコードを適宜編集して、config/initializersの下に置く。

📄config/initializers/reloader_rails_5.rb
unless Rails.env.production?
  reloader = Rails.application.config.file_watcher.new(['path_to_file']) do
    # do_something
  end
  Rails.application.reloaders << reloader

  ActiveSupport::Reloader.to_prepare do
    # reloader.execute <=他のファイルが更新されても実行するならこちら
    reloader.execute_if_updated
  end
end

なにをしているかというと、まずRails.application.config.file_watcherでファイルの更新監視クラスを取ってきている。普通はconfig/environments/development.rbには次のような設定がしてあるはずなので、

📄config/environments/development.rb
config.file_watcher = ActiveSupport::EventedFileUpdateChecker

ActiveSupport::EventedFileUpdateCheckerクラスが取得できるというわけだ。こいつに監視対象のファイルパスを渡し、ブロック内に実施したい処理を書けばよい。

次に、ActiveSupport::Reloader.to_prepare の部分だが、何らかのファイルに更新があった次のリクエスト処理時に、このブロックが実行されることになっている。ここで先ほど生成したreloaderを実行してやればよい。

reloader.execute_if_updatedというのは監視対象のファイルが更新された場合にだけreloaderを実行する。通常はこのメソッドを使えばよいと思う。他のファイルが更新された場合でもreloaderを実行したい場合はreloader.executeだ。

For Rails 4

基本的にはrails 5と同じなのだが、少しだけ使用するクラスが違う。

📄config/initializers/reloader_rails_4
unless Rails.env.production?
  reloader = ActiveSupport::FileUpdateChecker.new(['path_to_file']) do
    # do_something
  end
  Rails.application.reloaders << reloader
 
  ActionDispatch::Reloader.to_prepare do
    reloader.execute_if_updated
  end
end

やってることはrails 5と同じなので、説明は省略。

実例(rails 5)

📄config/initializers/reloader.rb
ymls = Dir.glob("#{Rails.root}/app/dsls/config/search/*.rb")
ymls.each { |yml| load yml }

unless Rails.env.production?
  ymls.each do |yml|
    reloader = Rails.application.config.file_watcher.new([yml]) do
      load yml
    end
    Rails.application.reloaders << reloader

    ActiveSupport::Reloader.to_prepare do
      reloader.execute
    end
  end
end

実際に私が使用している例。自作DSLがyamlファイルを読み込むのだが、yamlを変更する際にサーバを再起動しないでいいように、上のようにしてみた。

もともとは、settingslogicという定数管理用のgemを使用していたのだが、yamlを変更するとサーバを再起動しないといけないのが嫌で、解決策を探してみた。

ちなみに拙作の定数管理gemは、サーバを再起動しなくても、定数を記述するyamlを変更したら自動でリロードするようにしてある。

その中のここのソースでrails4と5を切り替えてreloaderを登録するようにしているので、gemを作る人なんかには参考になるかもしれない。

【Rails】リロード時に特定の処理を実行する Read More »

turbolinks 5 にtwitterボタンを対応させる

turbolinks 5を使用していると、ページの遷移時にtwitterのフォロー/シェアボタンが表示されなくなってしまう問題の解決方法。

解決方法

基本的にはCompatibilityのページに書いてあるとおりだが、順を追って説明していく。なおこれはCompatibilityのページのSolution #2に該当する。

まずはTwitterボタンのページでサイトに貼り付けたいボタンのソースをゲットする。通常はそいつをボタンを表示させたい箇所に貼り付ければいいわけだが、turbolinksを使用する場合は、ちょっと違う。取得したソースはaタグとscriptタグがあると思うのだが、そのうちaタグの方だけを貼り付けるようにする。下は@XXXXユーザーのフォローボタンの例。

📄フォローボタンのhtml
<a href="https://twitter.com/XXXX" class="twitter-follow-button" data-show-count="false" data-size="large">Follow @XXXX</a>

ボタンを貼り付けたらCoffeeScriptを作成する。ファイル名は任意。

📄app/assets/javascripts/twitter.coffee
twttr_events_bound = false

$ ->
  loadTwitterSDK ->
    bindTwitterEventHandlers() unless twttr_events_bound

bindTwitterEventHandlers = ->
  $(document).on 'turbolinks:load', renderTweetButtons
  twttr_events_bound = true

renderTweetButtons = ->
  $('.twitter-share-button').each ->
    button = $(this)
    button.attr('data-url', document.location.href) unless button.data('url')?
    button.attr('data-text', document.title) unless button.data('text')?
  twttr.widgets.load()

loadTwitterSDK = (callback) ->
  $.getScript('//platform.twitter.com/widgets.js', callback)

これだけ。

なお本家Compatibilityのサイトと若干CoffeeScriptのソースが異なるので注意。本家版はgetScriptに時間がかかった場合にエラーが出るようになっていたので、それを修正した。(PR出してるけどマージされるかは不明)

turbokinks 5、いまのところすごくいい感じ。パフォーマンスの向上を肌で感じることができてる(気分。)

turbolinks 5 にtwitterボタンを対応させる Read More »