lighthouse-batchを利用したURLリストに対するlighthouse実行メモ

Google Lighthouseはウェブサイト・ウェブアプリの品質向上に役立つツールで、ページスピードやSEO、PWAなど実装について計測、評価、アドバイスをもらうことができます。

ページ単体の評価であればChromeの拡張を利用したり、Chrome Devtoolsのauditを利用することで評価結果を見ることができますが、特定のURLリストについて片っ端に調べるとなると骨が折れます。

今回はnpmのlighthouse-batchを利用してみました。
lighthouse-batchを利用することでnpmのページにあるこれで一発で取得可能です。

lighthouse-batch -f sites.txt

sites.txtに改行区切りでURLのリストを入れておくだけで、あとは自動的に順番にチェックが走っていきます。
デフォルトでは出力データはjsonになります。
ファイルは report > lighthouse あたりに置かれていく事になるでしょう。
.
├── readme.md
├── report
│   └── lighthouse
│       └── __URL__.report.json
└── sites.txt

jsonファイルは各指定したURLに対応するものとは別に summary.json が出力されます。
summaryには全URLのスコアデータのみが載っており詳細は個別のjsonファイルを見ることとなりますが、summaryをBigQueryへ格納する場合のschemaはこんな感じで良いかと。雑ですがdetailだけ繰り返し無しのrecordで。

[
          {
            "name": "url",
            "type": "string"
          },
          {
            "name": "name",
            "type": "string"
          },
          {
            "name": "file",
            "type": "string"
          },
          {
            "name": "score",
            "type": "float"
          },
          {
            "name": "detail",
            "type": "record",
            "fields": [
              {
                "name": "performance",
                "type": "float"
              },
              {
                "name": "accessibility",
                "type": "float"
              },
              {
                "name": "best_practices",
                "type": "float"
              },
              {
                "name": "seo",
                "type": "float"
              },
              {
                "name": "pwa",
                "type": "float"
              }
            ]
          }
]


summary.jsonは1行1URLとなるように少し修正して、こんなフォーマットへ変換。

{"url":"https://www.google.com/","name":"www_google_com_","file":"www_google_com_.report.json","score":"0.70","detail":{"performance":0.47,"accessibility":0.93,"best_practices":0.79,"seo":0.99,"pwa":0.3}}
{"url":"https://www.google.com/","name":"www_google_com_","file":"www_google_com_.report.json","score":"0.70","detail":{"performance":0.47,"accessibility":0.93,"best_practices":0.79,"seo":0.99,"pwa":0.3}}

URL等はダミーです。1行1レコードになるので改行区切りで。カンマとか不要なものは取り除きます。もっと簡単なimportも可能なんだろうとは思いますが。
そのままbq loadします。

bq load --source_format=NEWLINE_DELIMITED_JSON mydataset.mytable summary.json

でsummaryのimportは終わりです。( mydataset.mytable の部分はBigqueryに用意したテーブルへ変換が必要)

今は個別のjsonは一旦GCSへ格納してしまっているのでsummaryでスコアを見て気になったページのレポートをGCSへ見に行くという行動をしているのですが、個別のjsonもBigqueryのテーブルに突っ込むというのもありなんだろうと思います。(summary.jsonのfileというのが詳細レポートのファイル名になっています)

ちなみにローカルにあるlighthouseレポートのjsonはviewerが用意されているので、ここで見るのが一番良さそう。

Lighthouse Report Viewer

たまにウェブサイトの健康診断の意味でも主要ページはlighthouseで確認をしたいところですね。

Dimensions & Metrics Explorerページの”Google Analytics Demos and Tools”移管

Google AnalyticsのCore Reporting APIでDimension名やMetric名を調べる際に調べるDimensions & Metrics Explorerページですが、今後Google Analytics Demos and Tools側に移管されるようです。


Notice: The Dimensions and metrics explorer is moving to Google Analytics Demos and Tools, and will be turned down in the coming weeks. Please try out the new site and report any issues you run into on the Issue Tracker.
他の開発側ページだとこの案内はなさそうに見えますがGoogle AnalyticsのAPI、コードまわりは一部developersから移管されていくのかもしれません。
Bookmarkしている場合はとりあえず変更しておくと良いかもです。

ウェブサイトの主要なURLのIndexチェックはCustom Search APIを利用する

SEOでGoogle検索のランキングチェックをしている企業や個人も多いと思います。Search Consoleでのレポートも多いため普段はそちらをチェックしているのですが、以前なんの拍子でか主要ページURLがIndexから外れていたという事がありました。Search Console側では数日後に検知されていましたが。

ただ、これを気に主要ページだけでもIndexチェックを自動的に回したいなと思い、毎月1回主要ページをのIndexチェックをCustom Search APIを利用することにしました。
Index調査は色々ツールもあるでしょうし、もっと簡単に分かる方法もあるとは思いますが今回はCustom Search APIを利用した場合です。
※1日100リクエストまで無料なので無料枠範囲内で毎日別のURLチェックをすることである程度低価格 OR 無料範囲内でチェック可能

ざっと適当なコードはこちら。


API_KEY = '' #consoleで発行したAPI KEY
CUSTOM_SEARCH_ENGINE = '' #Custom search engineで発行されたID

def get_url(search_keyword):
    urls = []
    query = 'https://www.googleapis.com/customsearch/v1?key=' + API_KEY + '&cx=' + CUSTOM_SEARCH_ENGINE + '&num=1&q=' + quote(search_keyword)
    res = urllib.request.urlopen(query)
    data = json.loads(res.read().decode('utf-8'))
    
    try:
        urls.append(data["items"][0]["link"])
        return urls
    except:
        # Indexされていないことを伝える通知とか

こんな感じでCustom Search APIを叩いていきます。
検索キーワードはGoogle先生おなじみの `site:` を利用していきます。

例) site:www.google.co.jp

で叩いているURLを見れば分かるように1位しか取得していません。1位のURLがIndexしたいURLと同一であればIndexされている。そうでなければされていないと判断でしていきます。

ただ必ず1位になるわけではない点は注意です。
適切にウェブサイトが整えられていて、サイト内リンク構造もキレイであれば基本的には1位になるわけですが、そうでない場合は1位に来ないのでそこは反省しましょう(?)

(参考)Custom Search JSON API

Google AnalyticsのRealtime APIの結果を雑にSlackに流す

Google AnalylticsのRealtime APIはGoogle Analytics画面のレポート>リアルタイムの数字をAPIで取得するものですが、公式ドキュメントにあるとおりベータで利用するには事前申請が必要...と思っていた訳ですが、普通にたたけばデータ取得できたのでこの数字を雑にSlackへ流すことにしました。

直近のイベントをSlackや別の何かですぐに気づきたい場合に利用できます。
V3のエンドポイントに対してRealtime API専用のdimensionsやmetricsを設定していく感じになります。無駄にDataFrameとか使っていますが出力を多少キレイにするためにtabulateパッケージを使ってSlackへ投稿していますので、以下のソースはサンプルというレベルです。

import httplib2
import json
import requests
import pandas as pd
from oauth2client.service_account import ServiceAccountCredentials


def ga_realtime():
    SCOPES = ['https://www.googleapis.com/auth/analytics.readonly']
    KEY_FILE_LOCATION = '' #file path

    credentials = ServiceAccountCredentials.from_json_keyfile_name(
        KEY_FILE_LOCATION, scopes=SCOPES)

    session = requests.Session()
    session.headers = {'Authorization': 'Bearer ' +
                       credentials.get_access_token().access_token}

    args = {
        'ids': 'ga:00000000000',  # viewID
        'metrics': 'rt:totalEvents',
        'dimensions': 'rt:eventCategory,rt:eventAction,rt:eventLabel',
        'filters': 'rt:eventAction==click'
    }

    response = session.get('https://www.googleapis.com/analytics/v3/data/realtime?ids={}&metrics={}&dimensions={}&filters={}'.format(
        args['ids'], args['metrics'], args['dimensions'], args['filters']))
    result = response.json()

    list = []

    try:
        rows = result['rows']
        for row in rows:
            list.append(row)
        df = pd.DataFrame(list, index=None, columns=[
                          'category', 'action', 'label', 'count'])

        WEB_HOOK_URL = 'https://hooks.slack.com/services/....'  # webhookurl
        requests.post(WEB_HOOK_URL, data=json.dumps({
            'text': df,
            'username': u'bot',
            'icon_emoji': u':robot_face:'
        }))
    except:
        pass

サンプルだとイベントデータを取得していて且つフィルタでアクションが `click` のものだけを取得しています。

今個人的にはGCP内、具体的にはGoogle Functionsで動かしており。KEYはGCSで管理してしまっているため上のコードだけではちゃんと動かないのですが。
サクッと作って誰かに通知したいとかそんな用途だと、このくらいのレベル感がとても良い気がします。

Google Analytics(無料版)データをAPI v4を利用してBigqueryへ入れる

marketechlabさんの無料版データを有料版と同じようにBigqueryへ格納する方法は凄くいいなぁと思いつつ、一長一短あるなぁと思いながら見ていました。
  • bot排除が必要(逆にメリットとしてbotの動きを知れる)
  • bigqueryへの格納方法にもよりますが分析コストが高いflat化したり
  • 既存GUIを見る事で足りるデータはあると思う(例えばブラウザバージョン分布等)
  • Bigqueryで舐めるデータ量が多そうなので工夫が必要かも?
bot以外の項目に関してはGAの有料版と同じだし、メリットも多い...ということでそちらへ切り替えようかどうしようか…と考えつつ、現状は力技で解決してしまっているため、今やっている方法も全くダメなんだよなぁといろいろ悩んでしまいました。

現状のデメリットとしては...
  • 分析したいデータという観点でデータの組み合わせ、抽出スパン等を事前に考える必要がある
  • 場合によってはlogだと1レコードで取得できる部分がテーブル違い等で重複して取得している場合がある
あたりでしょう。

■現状の構成

基本GCPで解決しているのは同じで、基本以下のような流れで進みます。現状だとStorageを挟む必要性もない構成なのですが、なんとなく噛ませてしまっていますね。あと鍵管理でKMSを利用していない部分が駄目。
  1. Cloud Scheduler
  2. Cloud Pub/Sub
  3. Cloud Functions
  4. Cloud Storage
  5. Bigquery
■取得データの取捨選択
キーはclientIDです。
  • clientIDベースの閲覧ページとページごとの滞在時間
  • clientIDベースのイベントデータ
  • clientIDベースの流入経路とランディングページ
他社と同様にcustom TaskでタイムスタンプとclientIDをセットでカスタムディメンションへ格納しています。

function() {
  return function(model) {
    var cid = model.get('clientId');
    var timestamp = new Date().getTime();
    model.set('dimension3',  timestamp + '_' + cid);
  }
}

最終的にBigqueryへ格納する際にこのくっつけている部分は分離しています。
分離はpythonのpandasを利用していてカスタムディメンション1に格納していた場合、そのデータを分割して元データをdropして整形しています。

analytics = initialize_analyticsreporting()
response = get_report(analytics, query)
df = get_dataframe(response)
ga_df = pd.concat([df['dimension1'].str.split('_', expand=True),df], axis = 1).drop('dimension1', axis = 1)

あとはデータの組み合わせの通りにGoogle Analytics V4経由でデータを抽出していきます。


#パターン1
'metrics': [{'expression': 'ga:sessions'},{'expression': 'ga:uniquePageviews'},{'expression': 'ga:pageviews'},{'expression':'ga:pageLoadTime'}],
'dimensions' : [{'name' : 'ga:dimension1'},{'name' : 'ga:pagePath'}]

#パターン2
'metrics': [{'expression': 'ga:uniqueEvents'}],
'dimensions' : [{'name' : 'ga:dimension1'},{'name' : 'ga:eventCategory'},{'name' : 'ga:eventAction'},{'name' : 'ga:eventLabel'}]

#パターン3
'metrics': [{'expression': 'ga:sessions'}],
'dimensions' : [{'name' : 'ga:dimension1'},{'name' : 'ga:channelGrouping'},{'name' : 'ga:source'},{'name' : 'ga:medium'},{'name' : 'ga:campaign'},{'name': 'ga:landingPagePath'}]

こんな組み合わせでデータを取得していっています。
dimensionは最大7、metricsは最大10まで指定できるので増やしたらBigqueryのカラムを追加するという感じでの拡張と新しいデータ取得を行ってテーブルを追加したりと拡張していく事を行っていけば良いと思っています。
最初は1テーブルでも良いですし後から分析したい項目内容にしたがって柔軟な対応が可能です。

デメリットもありますが、気軽に自分にあった内容で分析環境を整えていければ良いかなと思います。

[GTM]SPAで構築されたウェブサイトのハッシュ以下URLトラッキング

## tl;dr

SPAに分類されるウェブサイトでページ読み込みがなく、Google Analytics等のトラッキングができない場合はGTMトリガー「history change」を利用する

## 背景

React等を用いてSPAによるウェブフォームが作成され、それをトラッキングする必要がありました。そんなに多くのSPAを見てきた訳ではない & 自分でSPAのサンプル等を作ったこともなかったのでSPAだから共通して言える事なのかどうかはわかりませんが以下のような特徴がありました

* SPAなので当たり前ですがページ読み込みのない遷移
* ページ遷移後のURLが `#` の後ろにつく
* ウェブ履歴(history)には遷移毎にURLが載ってくる

## 必要な実装

### トラッキングに必要となる要件・要点

1. ページ遷移毎にGoogle Analytics等、トラッキングツールや広告ツールにログを送信すること
2. `#` 以下のURLをバーチャルページビューとしてURLトラッキングできるように実装すること

### ページ遷移毎のログ送信方法

#### 履歴トリガー

SPAでの実装時にブラウザのウェブ履歴にURLが掲載される事がポイント。これはAjaxが出てき5年以上前と全く同じ。SPAでもpushStateが利用されているのか、それともhistory api的ななにかが使われているのかは分かっていないのですが履歴トリガーを使います。

(参考)履歴の変更

### バーチャルページビュー対応

#### Google AnalyticsへのURL送信

Google Analyticsへバーチャルページビューを送信する場合 `Fileds to Set` で `location` を指定するか `page` を指定します。

#### URL整形

この部分は実装方法がサイトごとに異なるかもしれませんが私の場合、こんな感じの実装を行いました。

1. 【変数】 `#` 以降のハッシュ文字列を取得する - URL fragment



2. 【変数】 ハッシュURLを用いてURLを正規化する
こちらは方法は何通りもありますルックアップテーブルを使ってもいいし以下のように正規表現で書き換えても良いと思います。

※input variableや正規表現は適当なのでサイトに合わせて書いてください
※Lookup系テーブルは上から順に評価されることに注意してください

3. Google AnalyticsにURLを送信する
2でパスへ書き換えた場合は `Fields to Set` は `page` で、Step2で書き換えた変数を送信する。フルURLで書き換えた場合は `location` で送れば良いと思います。

## 注意事項

実装で個人的に考慮が漏れた点を1つ注意事項としてあげますが、トラッキング系含めURLにgetパラメータが付いてきた場合の挙動はちゃんと確認するべきでしょう。
最初Lookupテーブルを利用して実装していたのですが、そうすると様々なツールを通してget系パラメータ付で飛んでくるURLのパースがうまくいかない事にリリース直後気づきました。そこで正規表現テーブルへ移行してすぐにリカバリーしたのですがバーチャルURL生成時にちゃんとパラメータ付でも処理されるかは見たほうが良いでしょう。

## 結論

GTMのコードを埋め込んでもらっていれば自由に内部で処理できます。ハッシュ以下のURLも含めてきれいにトラッキング可能ですがツールによってバーチャルページビューをどう投げれば良いかは異なるので調べる必要があります。
ただ個人的にヒートマップツールなんかはバーチャルURLを利用するとデータ取得としては問題ないけれどもヒートマップとしてウェブページの上にレイヤー表示できない部分だけ解決出来ていないので、そういうツール毎に考慮が必要な部分が出てくるかもしれません。

Ajax実装でGTMを触っていたら `ブラウザ履歴を確認してみる` という行動をすると思いますが、全くの初めてだと迷うかもしれませんが、それほど難しい処理を書く必要は無いのではないかと思います。

## どうでもいいこと

GTMの正規表現テーブル、GAのフィルタみたいに `$A1` みたいな変な書き方ないよな?とか勘ぐって `GTM 正規表現テーブル  書き方` みたいなクエリでGoogle検索したのは内緒。

「電子商取引及び情報財取引等に関する準則」の改訂

経産省から出ていた「「電子商取引及び情報財取引等に関する準則」を改訂しました」というお知らせを見ていたのですが、スマートスピーカーまわりの電子商取引部分が追加されたということで眺めていました。
資料の中では「AIスピーカー」と呼ばれていますが、所謂スマートスピーカーのことですね。


1. AIスピーカーが誤認識した場合
⇒ 注文は無効。ユーザが確認を行って注文を確定するという確認措置を設ける事が有用。

この解説も結構面白くて、スマートスピーカーはユーザが自由にシステムの修正・入れ替えが出来ず事業者側に全て委ねられている事から「ユーザのエージェント」ではないと指摘されています。したがってユーザ自身の指示とは認められず契約は成立しないと。

また回避策として考えられる以下2点

  1. 一定時間内にユーザが注文に対し意思表示しなかったら注文を有効とする
  2. 一定時間内にユーザが注文に対し有効だと意思表示した場合のみ契約成立とみなす
については、1は無効となる可能性が高い。2は問題ない。
このあたりは消費者の利益を一方的に害するなど、既存の民法や消費者契約法に照らし合わせて判断されています。

2. AIスピーカーに対して発注者が言い間違いをした場合
⇒ 注文は無効だが、発注者側に重過失が認められる場合はその限りではない。

この解説で「電子契約法」について触れられているのですが、ちょっと内容忘れているので記載の文章を一部引用しますが

電子契約法第 2 条第 1 項の「電子消費者契約」の定義に①「映像面を介して締結される契約」という要件と②「当該映像面に表示する手続に従って消費者がその使用する電子計算機を用いて送信することによってその申込み又はその承諾の意思表示を行うもの」という要件が付されている
とあり、この「映像面を介して」という部分、前のスマートスピーカーだけでは契約は成立せずユーザに通知し、ユーザがその通知に対して有効な意思表示をした場合に注文が成立するよという内容と合わさって、うまいやり方沢山あるな!とハッとしましたw

例えば…スマートスピーカーで注文した直後にGoogleのログイン通知画面のように、その場でタップさせる画面を用意して、ここに注文内容を表示して、そのまま注文してもらうというのも良い手ですよね。(右の画像はGoogleのBlogより)

それよりも有効なのはAmazon Echo Spotですよね。



これ以上に最適なインターフェースは無いのかもしれないなと思いました。

Amazon Echo Spotは確か日本でも注文出来るようになるんですよね?
このあたりの法的整理が効いてきているのかも。
最近このAmazon Echo Spot、ポツポツ話を聞くようになってちょっと気になり始めました。