はじめに こんにちは、WEARフロントエンド部Webブロックの 新 です。普段は WEAR のWebサイトのリプレイス開発を担当しています。リプレイスを進める中で、不具合やリプレイス前後での変化にいち早く気づくため、Lighthouse CIによる日々の記録を可視化し定期的に通知する仕組みを作りました。本記事ではその取り組みについて詳しくご紹介します。 目次 はじめに 目次 背景・課題 Lighthouse CI とは やったこと 1. GitHub Actionsでページ毎にLighthouse CIを実行し、結果をGoogleスプレッドシートに保存 2. GoogleスプレッドシートをデータソースとしてLooker Studioでグラフ化 3. Looker StudioからGoogle Apps Scriptを使用するアカウントへメール配信 4. Google Apps ScriptでLooker Studioからの最新メールを取得し、添付されている画像をSlackへ転送 おわりに 背景・課題 WEARのWebフロントエンドチームでは、10年来のVBScriptの環境からNext.jsへのリプレイスを進めています。WEARのWebサイトはオーガニック検索からの流入で閲覧してくださるユーザーがとても多いため、リプレイスを行う動機として開発生産性の向上の他にSEOの改善もありました。詳細は下記の記事をご覧ください。 techblog.zozo.com SEO改善のため、Lighthouseによる計測を継続的に行うべきと判断し、Lighthouse CIによるスコアの蓄積及び比較をすることにしました。また、リプレイス後環境のリグレッションに気づくためにも、日々のLighthouseスコアを可視化するべきだと考えました。 Lighthouse CI とは まず、 Lighthouse はGoogleが提供するWebページの品質を測定するツールで、 Lighthouse CI はそのLighthouseによる測定結果を継続的に取得するための支援ツールです。 Lighthouse CIには、Lighthouse CI CLIとLighthouse CI Serverというパッケージがあります。 Lighthouse CI Server はLighthouse CI CLIの実行結果を、蓄積、可視化まで一括して行うことができるパッケージです。しかし、今回はLighthouse CI Serverを用いずLooker Studioによる可視化を採用しました。Lighthouse CI Serverはセルフホスティングを前提としており、今回の用途ではリグレッションに気づくことができれば十分なのでコスト的な面で採用しませんでした。 やったこと 課題を解決するため「定期的に、Lighthouseによる解析・スコアの収集・グラフ化・Slackへ通知する」という仕組みを作りました。その仕組みの流れを図で表したものがこちらです。 GitHub Actionsでページ毎にLighthouse CIを実行し、結果をGoogleスプレッドシートに保存 GoogleスプレッドシートをデータソースとしてLooker Studioでグラフ化 Looker StudioからGoogle Apps Scriptを使用するアカウントへメール配信 Google Apps ScriptでLooker Studioからの最新メールを取得し、添付されている画像をSlackへ転送 1. GitHub Actionsでページ毎にLighthouse CIを実行し、結果をGoogleスプレッドシートに保存 まず、GitHub ActionsでLighthouse CIを実行すると、各ページの計測結果は .lighthouseci ディレクトリにJSON形式で格納されます。次に、格納されたJSONを読み込みGoogleスプレッドシートへ保存するスクリプトを実行します。 .lighthouseci ディレクトリに格納された計測結果は以下のようなコードで取得できます。 import { readdir, readFile } from "fs/promises" ; import { join } from "path" ; /** * 指定ディレクトリ内のlhr jsonファイルのパス一覧を取得 */ const listLhrJson = ( dir : string ): Promise < string []> => readdir(dir, { withFileTypes : true } ). then (( entries ) => entries . filter (( e ) => e.isFile() && e. name . match ( /^lhr-\w+\.json$/ )) . map (( e ) => join(dir, e. name )) ); const jsonPaths = await listLhrJson( ".lighthouseciディレクトリのパス" ); const resultTexts = await Promise . all ( jsonPaths. map (( p ) => readFile(p, "utf8" )) ); const results = resultTexts. map (( resultText ) => JSON . parse (resultText)); 上の results は以下のようなページごとの配列になっています。 [ { lighthouseVersion : "12.1.0" , requestedUrl : "https://wear.jp/" , // ... categories : { performance : { // ... score : 0.82 , } , accessibility : { // ... score : 0.75 , } , "best-practices" : { // ... score : 0.78 , } , seo : { // ... score : 0.85 , } , } , // ... } , // ... ] ; 上記の取得した結果を次のように加工します。 [ [ "https://wear.jp/" , { performance : 51 , accessibility : 73 , seo : 83 , "best-practices" : 59 , } , ] , // ... ] ; 加工した結果を次の関数に渡し、Googleスプレッドシートに保存します。 import { AuthClient } from "google-auth-library" ; import { GoogleSpreadsheet } from "google-spreadsheet" ; const storeToSpreadsheet = async ( sheetId : string , auth : AuthClient , urlAvgMap : [ string , Record < string , number > ] [] ) => { const doc = new GoogleSpreadsheet(sheetId, auth); const date = new Date (). toISOString (); await doc.loadInfo(); const firstSheet = doc.sheetsByIndex[ 0 ]; await firstSheet.loadHeaderRow(); const rows = urlAvgMap. map (( [url , avgs] ) => ( { url , date , ...avgs, } )); await firstSheet.addRows(rows); } ; 2. GoogleスプレッドシートをデータソースとしてLooker Studioでグラフ化 Lighthouseのスコアを保存したGoogleスプレッドシートをデータソースとして、Looker Studioでグラフ化していきます。Looker Studioでのデータの連携やレポートの詳しい作成手順は下記記事をご覧ください。 techblog.zozo.com 実際に蓄積したデータをLooker Studioでグラフ化したものの一部がこちらです。下のメンバー詳細はちょうどリプレイスを行ったばかりのページなため、SEOやパフォーマンスなどが改善されているのがわかると思います。 パフォーマンスなどのスコアにバラつきがあるのは、GitHub Actionsが実行される環境の違いです。GitHubホステッドランナーを利用しているため、割り当てられるインスタンスのスペックも様々でスコアにバラつきが出てしまっています。私たちの用途では、Lighthouse CIから計測したグラフを異常な変化の検知に用いているため、多少のバラつきは大きな問題ではありません。 3. Looker StudioからGoogle Apps Scriptを使用するアカウントへメール配信 Looker Studioでグラフ化した結果の画像をSlackへ転送するため、メール配信の設定をします。Looker Studioから画像を取得するためのAPIは無くメール配信のみのため、メール経由でSlackに転送します。そのためにLooker Studioから通知したいページや通知頻度を設定します。 4. Google Apps ScriptでLooker Studioからの最新メールを取得し、添付されている画像をSlackへ転送 Looker Studioから配信されたメールの画像を抜き取り、Slackへ転送します。Google Apps Scriptで書いた次のコードは、Looker Studioから配信されたメールの画像データを抜き取る関数の例です。メールは設定したタイトルをもとに取得します。 function getPhotosData () { const reportTitle = "Lighthouse 週次レポート" ; const gmailThread = GmailApp . search ( `from:looker-studio-noreply@google.com subject: ${ reportTitle } ` ) ; const messages = gmailThread [ 0 ] . getMessages () ; const attachments = messages [ 0 ] . getAttachments ({ includeInlineImages : true , includeAttachments : false , }) ; return attachments . map (( attachment ) => { return { blob : attachment . getAs ( attachment . getContentType ()) , name : attachment . getName () , contentType : attachment . getContentType () , } ; }) ; } 次に、取得した画像データをSlackに転送するコードの例です。 getFileUploadUrl で画像データからアップロード用のURLを取得し、取得したURLにPOSTリクエストを送信してファイルをアップロードします。 completeFileUpload に getFileUploadUrl から受け取ったファイルIDを渡してアップロード処理を完了させます。 // ファイルアップロード用URLを取得する function getFileUploadUrl ( filename , length ) { const options = { method : "get" , headers : { Authorization : "Bearer " + PropertiesService . getScriptProperties () . getProperty ( "SLACK_TOKEN" ) , } , payload : { filename : filename , length : length . toString () , } , } ; const response = UrlFetchApp . fetch ( "https://slack.com/api/files.getUploadURLExternal" , options ) ; const responseBody = response . getContentText () ; const data = JSON . parse ( responseBody ) ; if ( ! data . ok ) { throw new Error ( data ) ; } return [ data . upload_url , data . file_id ] ; } // ファイルアップロード処理を完了する function completeFileUpload ( fileId ) { const options = { method : "post" , headers : { Authorization : "Bearer " + PropertiesService . getScriptProperties () . getProperty ( "SLACK_TOKEN" ) , } , contentType : "application/json" , payload : JSON . stringify ({ files : [{ id : fileId }] , channel_id : CHANNEL_ID , initial_comment : `< ${ LookerStudioReportURL } | ${ WEAR Lighthouseレポート } >` }) , } ; const response = UrlFetchApp . fetch ( "https://slack.com/api/files.completeUploadExternal" , options ) ; const responseBody = response . getContentText () ; const data = JSON . parse ( responseBody ) ; if ( ! data . ok ) { throw new Error ( data ) ; } } // ファイルをアップロードする function uploadFile ( photoData ) { const [ fileUploadUrl , fileId ] = getFileUploadUrl ( photoData . name , photoData . blob . getBytes () . length ) ; const options = { method : "post" , payload : photoData . blob , } ; const response = UrlFetchApp . fetch ( fileUploadUrl , options ) ; completeFileUpload ( fileId ) ; } function main () { const photos = getPhotosData () ; photos . forEach (( photo ) => { uploadFile ( photo ) ; }) ; } 最後に、上記のコードを実行させるため、Google Apps Scriptで main 関数の実行タイミングを設定します。実行タイミングはLooker Studioのメール配信で設定した時刻より後に設定する必要があります。 実行されると以下のようにSlackの指定したチャンネルで通知されるようになります。 おわりに 本記事では「定期的に、Lighthouseによる解析・スコアの収集・グラフ化・Slackへ通知する」仕組みについて紹介しました。この仕組みを導入にしたことによって、リプレイス後環境のリグレッションにいち早く気づくことができるようになりました。現在はGitHub Actions上でLighthouse CIを実行していますが、AWS EC2のスポットインスタンスを利用するなど、コストの削減やスコア変動の改善にも取り組みたいです。日々のLighthouseのスコアの定点観測を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com