ようへいの日々精進XP

よかろうもん

最近ギョームでやったこと (6) 〜 一歩踏み込んだ Web サイトの監視について考察して仮実装してみた 〜

tl;dr

お疲れ様です. かっぱです. YAMAP に入社してもうすぐ一年が経とうとしています. あっと言う間の一年でした. この一年の振り返りを... と思いましたが, 最近の悩み事を技術で解決してみようと思って試験的に実装した内容を紹介させていただきます.

そして, この記事は, YAMAP エンジニア Advent Calendar 2019 の 25 日目の記事になる予定です.

qiita.com

悩み事

YAMAP では, yamap.com 以外にもいくつかの外部サイトを運営しています.

これらのサイトのデプロイ作業を含む各種運用に携わっていますが, これらのサイトがデプロイの度にレイアウトがデグレしてたり, 意図しないような状態で表示されたりすることが多く発生していました. 原因はいくつかありますが, コンテンツが CVS の管理外であったり, コンテンツをオンラインで更新していて CVS にマージされていなかったり人為的な手違いによるものが多く, さらに, その異常に気付くことが出来なかったりして胃が痛くなる日々が続いていました. ということで, ざっくりと悩み事をまとめると...

  • 意図しないサイトのデグレが度々発生する
  • ソースコードとコンテンツが別々に管理されている (WordPress で記事はデータベース, テーマのソースコードCVS で管理) ので変更検知は 2 箇所になるので, 最後に信じられるのは実際に動いている環境になりがち
  • コンテンツ管理者とテーマの管理者が異なる
  • HTTP ステータスコードレベルでの監視では満たされてない

こんな感じです.

どんな風に解決しようと試みたか

下図のような実装を考えて実装しました. (雑に手書きですいません.)

f:id:inokara:20191225211402j:plain

流れとしては...

こんな感じ. 上図では Puppeteer ってなっているところは, 実際には Jasmine から Puppeteer を操作してスクリーンショットを取得して, 従来のスクリーンショットとの差分を比較する処理が動きます.

作ったもの

リポジトリ

github.com

こだわり

出来るだけ手を動かすことなく CircleCI 上で動かすことを考えました.

また, Puppeteer でサイトにアクセスしてスクリーンショットを取得して比較するコアな部分は最小限の実装に スクリーンショットを S3 にアップロード, ダウンロードする部分はコアとは別に AWS CLI (実際には CircleCI の orbs) で実装しました. 以下は .circleci/config.yml の抜粋です.

version: 2.1

orbs:
  aws-s3: circleci/aws-s3@1.0.11
  slack: circleci/slack@3.4.1

executors:
  default:
    docker:
      - image: circleci/node:12.4
      - image: circleci/python:2.7

commands:
  check_prepare:
    steps:
      - aws-s3/sync:
          from: s3://${ROOT_PATH}/
          to: /home/circleci/project/${ROOT_PATH}/
          overwrite: true
          arguments: >
            --delete
  npm_install:
    steps:
      - run:
          name: Update npm
          command: sudo npm install -g npm@latest
      - restore_cache:
          name: Restore Dependencies
          keys:
            - dependencies-{{ checksum "package-lock.json" }}
            - dependencies
      - run:
          name: Install Dependencies
          command: npm install
      - save_cache:
          name: Save Dependencies
          key: dependencies-{{ checksum "package-lock.json" }}
          paths:
            - node_modules

jobs:
  check:
    executor:
      name: default
    steps:
      - checkout
      - run:
          name: Install Headless Chrome dependencies
          command: |
            sudo apt-get install -yq \
... 略 ...
      - npm_install
      - check_prepare
      - run:
          name: Run test
          command: ./node_modules/.bin/jasmine
... 略 ...
      - run:
          name: Store Images
          command: |
            aws s3 sync /home/circleci/project/${ROOT_PATH}/ s3://${ROOT_PATH}/
          when: always
      - slack/status:
          fail_only: true
          mentions: ${SLACK_MEMBER_IDS}

workflows:
  version: 2
  byhand-webpage-check:
    jobs:
      - check
  triggerd-webpage-check:
    triggers:
      - schedule:
          cron: "3 * * * *"
          filters:
            branches:
              only:
                - master
    jobs:
      - check

また, CircleCI の triggers を利用して一時間に一回定期的に実行させるようにしています.

circleci.com

さらに, S3 へのアップロードと Slack への通知は orbs を利用しています.

github.com

github.com

ちょっと痒いところに手が届かないところ (run で利用出来る when 的な定義が書けないとか) が残念でしたが, 基本的な使い方であれば全然 orbs でいけるので最高ですな.

苦労したところ

Puppeteer (1)

何よりも Puppeteer です. 全く触ったことなかったので, 見様見真似で写経 (コピペ) していました. しかし, 以下のように簡単にサイトのスクリーンショットを取得出来るのには感動しました.

//
// https://github.com/puppeteer/puppeteer#usage より引用
//
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://yamap.com');
  await page.screenshot({path: 'test.png'});

  await browser.close();
})();

これを test.js あたりで保存して node test.js とすると, 以下のようなスクリーンショットがシュッと撮れます.

f:id:inokara:20191225214856p:plain

しかし, 相手は JavaScript, 一筋縄ではいきません. asyncawait がよくわからないまま書き進めてしまい, 一応, 思ったような動作になりました... これでは, 成長が無いのでもう少し asyncawait をはじめ JavaScript について理解を深めていきたいと思います.

Puppeteer (2)

ページ全体のスクリーンショットを取得したい場合, 以下のように screenshot メソッドの引数に fullPage: true をつけてあげると取得出来ることは確認しました.

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://xxxx.xxxx.com/');
  await page.screenshot({path: 'test.png', fullPage: true});

  await browser.close();
})();

ところが, サイトによっては, 下図のように画像が一部読み込まれず悩みました.

f:id:inokara:20191225235519p:plain

現在も解決出来ていません...

差分抽出

差分抽出には Resemble.js を利用しています.

github.com

この Resemble.js も Puppeteer 同様に以下のように簡単に画像の差分を抽出することが出来ます.

        resemble('after.png').compareTo('before.png')
          .ignoreColors()
          .onComplete(function(data) {
              fse.writeFileSync(DirName + 'diff.png', data.getBuffer());
          });

動いている様子

CircleCi でのテスト結果.

f:id:inokara:20200106104657p:plain

上記のように, 対象ページに差分があり, 差分許容率がしきい値を超えるとテストが Fail となります.

f:id:inokara:20191225221726p:plain

Fail したら Slack に上図のように通知されます. 通知が届いたら, S3 バケットに保存されている画像を確認して, 意図した差分なので, そうでないかをデザイナーさん等に確認する流れになることを想定しています.

f:id:inokara:20191225221116p:plain

また, テストと合わせて差分抽出画像が生成されます. 差分抽出画像は上図のような画像になります. 前回のスクリーンショットとの差分がピンク色に着色された状態になります.

参考

以下の記事を大幅に参考にさせていただきました. Puppeteer のことを 1bit もわからない自分でもなんとなく利用することが出来ました. ありがとうございました.

qiita.com

qiita.com

ということで

まとめ

Web サイトの監視について, 対象のコンテンツ差分をチェックするというアプローチを検討して実装してみました. 意図したコンテンツの変更についても検知してアラートが飛んでくる状況ですが, これはこれで外形監視上の変更履歴として利用出来るのではと考えています.

実装にあたり, 使った技術要素を見てみると, チェックのフレームワークとして Jasmine というテストフレームワーク (と言っていいのかわかりませんが), 実際の基盤としては CircleCI と出来合いのフレームワークを組み合わせることでシュッと仮実装まで持ってくることが出来ました. 技術の進化というものは本当に素晴らしいものですね.

最後に

YAMAP に入社して一年が経とうとしていますが, エンジニアとして, 自分なりの「新しい山を作ろう」で頑張っていきたいと思います. これからも宜しくお願い致します.