仕事中の問題と解決メモ。

最近はPythonとGoogle Cloud Platformがメイン。株式会社ビズオーシャンで企画と開発運用、データ活用とか。https://github.com/uyamazak/

Googleアナリティクス プレミアム(360)のコストが高すぎるなら自分で作ればいい

jsタグを貼るだけで無料で高度なアクセス解析ができるGoogle Analyticsはもはやデファクトスタンダード

でも、個人を特定できるような生ログは無料版では手に入れられず、自社の会員IDと結びつけたり、特定の人の行動を追ったりすることは出来ない。

そこで、有料版のGoogle Analyticsプレミアム(Google Analytics 360?)なら、生ログをBigQueryにエクスポートできるので色々できるんだけれど、いかんせん値段が高すぎる。


ちょっと古い2012年の記事(1ドル80円計算が懐かしい)だと月100万

GoogleアナリティクスPremiumとは? 有料版GAの特徴・機能・対象を解説! | GoogleアナリティクスPremium【短期集中連載】 | Web担当者Forum


公式を見ると現在は段階的な価格設定になっているらしい。
www.google.com

でも、代理店が必須だし、そこの取り分などを考えると最低でも数十万単位はかかりそう。

ちなみに、GAプレミアムをBigQueryにエクスポートするとどんなログが取れるかというとサンプルのデータセットが公開されており、見ることができる。

データセット名は下記。

google.com:analytics-bigquery

テキストで書き出してみると、下記のようなRECORD型のREPEATED(入れ子)を複数持った、とても複雑なスキーマ定義がされているのが分かる。
※サンプルデータは2013年となっているので、もしかしたら現在は違っているかもしれない。

visitorId	INTEGER	NULLABLE
visitNumber	INTEGER	NULLABLE
visitId	INTEGER	NULLABLE
visitStartTime	INTEGER	NULLABLE
date	STRING	NULLABLE
totals	RECORD	NULLABLE
    totals.visits	INTEGER	NULLABLE
    totals.hits	INTEGER	NULLABLE
    totals.pageviews	INTEGER	NULLABLE
    totals.timeOnSite	INTEGER	NULLABLE
    totals.bounces	INTEGER	NULLABLE
    totals.transactions	INTEGER	NULLABLE
    totals.transactionRevenue	INTEGER	NULLABLE
    totals.newVisits	INTEGER	NULLABLE
trafficSource	RECORD	NULLABLE
    trafficSource.referralPath	STRING	NULLABLE
    trafficSource.campaign	STRING	NULLABLE
    trafficSource.source	STRING	NULLABLE
    trafficSource.medium	STRING	NULLABLE
    trafficSource.keyword	STRING	NULLABLE
    trafficSource.adContent	STRING	NULLABLE
device	RECORD	NULLABLE
    device.browser	STRING	NULLABLE
    device.browserVersion	STRING	NULLABLE
    device.operatingSystem	STRING	NULLABLE
    device.operatingSystemVersion	STRING	NULLABLE
    device.isMobile	BOOLEAN	NULLABLE
    device.flashVersion	STRING	NULLABLE
    device.javaEnabled	BOOLEAN	NULLABLE
    device.language	STRING	NULLABLE
    device.screenColors	STRING	NULLABLE
    device.screenResolution	STRING	NULLABLE
customDimensions	RECORD	REPEATED
    customDimensions.index	INTEGER	NULLABLE
    customDimensions.value	STRING	NULLABLE
hits	RECORD	REPEATED
    hits.hitNumber	INTEGER	NULLABLE
    hits.time	INTEGER	NULLABLE
    hits.hour	INTEGER	NULLABLE
    hits.minute	INTEGER	NULLABLE
    hits.isSecure	BOOLEAN	NULLABLE
    hits.isInteraction	BOOLEAN	NULLABLE
    hits.referer	STRING	NULLABLE
hits.page	RECORD	NULLABLE
    hits.page.pagePath	STRING	NULLABLE
    hits.page.hostname	STRING	NULLABLE
    hits.page.pageTitle	STRING	NULLABLE
    hits.page.searchKeyword	STRING	NULLABLE
    hits.page.searchCategory	STRING	NULLABLE
hits.transaction	RECORD	NULLABLE
    hits.transaction.transactionId	STRING	NULLABLE
    hits.transaction.transactionRevenue	INTEGER	NULLABLE
    hits.transaction.transactionTax	INTEGER	NULLABLE
    hits.transaction.transactionShipping	INTEGER	NULLABLE
    hits.transaction.affiliation	STRING	NULLABLE
    hits.transaction.currencyCode	STRING	NULLABLE
    hits.transaction.localTransactionRevenue	INTEGER	NULLABLE
    hits.transaction.localTransactionTax	INTEGER	NULLABLE
    hits.transaction.localTransactionShipping	INTEGER	NULLABLE
hits.item	RECORD	NULLABLE
    hits.item.transactionId	STRING	NULLABLE
    hits.item.productName	STRING	NULLABLE
    hits.item.productCategory	STRING	NULLABLE
    hits.item.productSku	STRING	NULLABLE
    hits.item.itemQuantity	INTEGER	NULLABLE
    hits.item.itemRevenue	INTEGER	NULLABLE
    hits.item.currencyCode	STRING	NULLABLE
    hits.item.localItemRevenue	INTEGER	NULLABLE
hits.contentInfo	RECORD	NULLABLE
    hits.contentInfo.contentDescription	STRING	NULLABLE
hits.appInfo	RECORD	NULLABLE
    hits.appInfo.name	STRING	NULLABLE
    hits.appInfo.version	STRING	NULLABLE
    hits.appInfo.id	STRING	NULLABLE
    hits.appInfo.installerId	STRING	NULLABLE
hits.exceptionInfo	RECORD	NULLABLE
    hits.exceptionInfo.description	STRING	NULLABLE
    hits.exceptionInfo.isFatal	BOOLEAN	NULLABLE
hits.eventInfo	RECORD	NULLABLE
    hits.eventInfo.eventCategory	STRING	NULLABLE
    hits.eventInfo.eventAction	STRING	NULLABLE
    hits.eventInfo.eventLabel	STRING	NULLABLE
    hits.eventInfo.eventValue	INTEGER	NULLABLE
hits.customVariables	RECORD	REPEATED
    hits.customVariables.index	INTEGER	NULLABLE
    hits.customVariables.customVarName	STRING	NULLABLE
    hits.customVariables.customVarValue	STRING	NULLABLE
hits.customDimensions	RECORD	REPEATED
    hits.customDimensions.index	INTEGER	NULLABLE
    hits.customDimensions.value	STRING	NULLABLE
hits.customMetrics	RECORD	REPEATED
    hits.customMetrics.index	INTEGER	NULLABLE
    hits.customMetrics.value	INTEGER	NULLABLE
hits.type	STRING	NULLABLE
fullVisitorId	STRING	NULLABLE

データのプレビューは横に長過ぎて出せないので、JSON型で一つだけ出してみる。

[
  {
    "visitorId": null,
    "visitNumber": "1",
    "visitId": "1378805776",
    "visitStartTime": "1378805776",
    "date": "20130910",
    "totals": {
      "visits": "1",
      "hits": "8",
      "pageviews": "5",
      "timeOnSite": "468",
      "bounces": null,
      "transactions": null,
      "transactionRevenue": null,
      "newVisits": "1"
    },
    "trafficSource": {
      "referralPath": "/urbancycling/reviews/foldable-helmet-from-lch.html",
      "campaign": null,
      "source": "technologysauce.com",
      "medium": "referral",
      "keyword": null,
      "adContent": null
    },
    "device": {
      "browser": "Firefox",
      "browserVersion": "23.0",
      "operatingSystem": "Linux",
      "operatingSystemVersion": "x86_64",
      "isMobile": "false",
      "flashVersion": "(not set)",
      "javaEnabled": "false",
      "language": "en-us",
      "screenColors": "24-bit",
      "screenResolution": "1920x1200"
    },
    "customDimensions": [],
    "hits": [
      {
        "hitNumber": "1",
        "time": "0",
        "hour": "9",
        "minute": "36",
        "isSecure": null,
        "isInteraction": null,
        "referer": null,
        "page": {
          "pagePath": "/helmets/foldable.html",
          "hostname": "londoncyclehelmet.com",
          "pageTitle": "London Cycle Helmet - Helmets - Foldable",
          "searchKeyword": null,
          "searchCategory": null
        },
        "transaction": {
          "transactionId": null,
          "transactionRevenue": null,
          "transactionTax": null,
          "transactionShipping": null,
          "affiliation": null,
          "currencyCode": null,
          "localTransactionRevenue": null,
          "localTransactionTax": null,
          "localTransactionShipping": null
        },
        "item": {
          "transactionId": null,
          "productName": null,
          "productCategory": null,
          "productSku": null,
          "itemQuantity": null,
          "itemRevenue": null,
          "currencyCode": null,
          "localItemRevenue": null
        },
        "contentInfo": {
          "contentDescription": null
        },
        "appInfo": {
          "name": null,
          "version": null,
          "id": null,
          "installerId": null
        },
        "exceptionInfo": {
          "description": null,
          "isFatal": "false"
        },
        "eventInfo": {
          "eventCategory": null,
          "eventAction": null,
          "eventLabel": null,
          "eventValue": null
        },
        "customVariables": [],
        "customDimensions": [
          {
            "index": "1",
            "value": "Helmets"
          }
        ],
        "customMetrics": [],
        "type": "PAGE"
      },
      {
        "hitNumber": "2",
        "time": "6998",
        "hour": "9",
        "minute": "36",
        "isSecure": null,
        "isInteraction": null,
        "referer": null,
        "page": {
          "pagePath": "/helmets/foldable.html",
          "hostname": "londoncyclehelmet.com",
          "pageTitle": "London Cycle Helmet - Helmets - Foldable",
          "searchKeyword": null,
          "searchCategory": null
        },
        "transaction": {
          "transactionId": null,
          "transactionRevenue": null,
          "transactionTax": null,
          "transactionShipping": null,
          "affiliation": null,
          "currencyCode": null,
          "localTransactionRevenue": null,
          "localTransactionTax": null,
          "localTransactionShipping": null
        },
        "item": {
          "transactionId": null,
          "productName": null,
          "productCategory": null,
          "productSku": null,
          "itemQuantity": null,
          "itemRevenue": null,
          "currencyCode": null,
          "localItemRevenue": null
        },
        "contentInfo": {
          "contentDescription": null
        },
        "appInfo": {
          "name": null,
          "version": null,
          "id": null,
          "installerId": null
        },
        "exceptionInfo": {
          "description": null,
          "isFatal": "false"
        },
        "eventInfo": {
          "eventCategory": "View",
          "eventAction": "LargeImage",
          "eventLabel": "Foldable Helmet",
          "eventValue": null
        },
        "customVariables": [],
        "customDimensions": [
          {
            "index": "1",
            "value": "Helmets"
          }
        ],
        "customMetrics": [],
        "type": "EVENT"
      },
      {
        "hitNumber": "3",
        "time": "11249",
        "hour": "9",
        "minute": "36",
        "isSecure": null,
        "isInteraction": null,
        "referer": null,
        "page": {
          "pagePath": "/helmets/foldable.html",
          "hostname": "londoncyclehelmet.com",
          "pageTitle": "London Cycle Helmet - Helmets - Foldable",
          "searchKeyword": null,
          "searchCategory": null
        },
        "transaction": {
          "transactionId": null,
          "transactionRevenue": null,
          "transactionTax": null,
          "transactionShipping": null,
          "affiliation": null,
          "currencyCode": null,
          "localTransactionRevenue": null,
          "localTransactionTax": null,
          "localTransactionShipping": null
        },
        "item": {
          "transactionId": null,
          "productName": null,
          "productCategory": null,
          "productSku": null,
          "itemQuantity": null,
          "itemRevenue": null,
          "currencyCode": null,
          "localItemRevenue": null
        },
        "contentInfo": {
          "contentDescription": null
        },
        "appInfo": {
          "name": null,
          "version": null,
          "id": null,
          "installerId": null
        },
        "exceptionInfo": {
          "description": null,
          "isFatal": "false"
        },
        "eventInfo": {
          "eventCategory": "Basket",
          "eventAction": "Add",
          "eventLabel": "Foldable Helmet",
          "eventValue": null
        },
        "customVariables": [],
        "customDimensions": [
          {
            "index": "1",
            "value": "Helmets"
          }
        ],
        "customMetrics": [],
        "type": "EVENT"
      },
      {
        "hitNumber": "4",
        "time": "17466",
        "hour": "9",
        "minute": "36",
        "isSecure": null,
        "isInteraction": null,
        "referer": null,
        "page": {
          "pagePath": "/",
          "hostname": "londoncyclehelmet.com",
          "pageTitle": "London Cycle Helmet",
          "searchKeyword": null,
          "searchCategory": null
        },
        "transaction": {
          "transactionId": null,
          "transactionRevenue": null,
          "transactionTax": null,
          "transactionShipping": null,
          "affiliation": null,
          "currencyCode": null,
          "localTransactionRevenue": null,
          "localTransactionTax": null,
          "localTransactionShipping": null
        },
        "item": {
          "transactionId": null,
          "productName": null,
          "productCategory": null,
          "productSku": null,
          "itemQuantity": null,
          "itemRevenue": null,
          "currencyCode": null,
          "localItemRevenue": null
        },
        "contentInfo": {
          "contentDescription": null
        },
        "appInfo": {
          "name": null,
          "version": null,
          "id": null,
          "installerId": null
        },
        "exceptionInfo": {
          "description": null,
          "isFatal": "false"
        },
        "eventInfo": {
          "eventCategory": null,
          "eventAction": null,
          "eventLabel": null,
          "eventValue": null
        },
        "customVariables": [],
        "customDimensions": [],
        "customMetrics": [],
        "type": "PAGE"
      },
      {
        "hitNumber": "5",
        "time": "20211",
        "hour": "9",
        "minute": "36",
        "isSecure": null,
        "isInteraction": null,
        "referer": null,
        "page": {
          "pagePath": "/vests/",
          "hostname": "londoncyclehelmet.com",
          "pageTitle": "London Cycle Helmet - Helmets",
          "searchKeyword": null,
          "searchCategory": null
        },
        "transaction": {
          "transactionId": null,
          "transactionRevenue": null,
          "transactionTax": null,
          "transactionShipping": null,
          "affiliation": null,
          "currencyCode": null,
          "localTransactionRevenue": null,
          "localTransactionTax": null,
          "localTransactionShipping": null
        },
        "item": {
          "transactionId": null,
          "productName": null,
          "productCategory": null,
          "productSku": null,
          "itemQuantity": null,
          "itemRevenue": null,
          "currencyCode": null,
          "localItemRevenue": null
        },
        "contentInfo": {
          "contentDescription": null
        },
        "appInfo": {
          "name": null,
          "version": null,
          "id": null,
          "installerId": null
        },
        "exceptionInfo": {
          "description": null,
          "isFatal": "false"
        },
        "eventInfo": {
          "eventCategory": null,
          "eventAction": null,
          "eventLabel": null,
          "eventValue": null
        },
        "customVariables": [],
        "customDimensions": [
          {
            "index": "1",
            "value": "Vests"
          }
        ],
        "customMetrics": [],
        "type": "PAGE"
      },
      {
        "hitNumber": "6",
        "time": "22695",
        "hour": "9",
        "minute": "36",
        "isSecure": null,
        "isInteraction": null,
        "referer": null,
        "page": {
          "pagePath": "/vests/yellow.html",
          "hostname": "londoncyclehelmet.com",
          "pageTitle": "London Cycle Helmet - Vests - Yellow",
          "searchKeyword": null,
          "searchCategory": null
        },
        "transaction": {
          "transactionId": null,
          "transactionRevenue": null,
          "transactionTax": null,
          "transactionShipping": null,
          "affiliation": null,
          "currencyCode": null,
          "localTransactionRevenue": null,
          "localTransactionTax": null,
          "localTransactionShipping": null
        },
        "item": {
          "transactionId": null,
          "productName": null,
          "productCategory": null,
          "productSku": null,
          "itemQuantity": null,
          "itemRevenue": null,
          "currencyCode": null,
          "localItemRevenue": null
        },
        "contentInfo": {
          "contentDescription": null
        },
        "appInfo": {
          "name": null,
          "version": null,
          "id": null,
          "installerId": null
        },
        "exceptionInfo": {
          "description": null,
          "isFatal": "false"
        },
        "eventInfo": {
          "eventCategory": null,
          "eventAction": null,
          "eventLabel": null,
          "eventValue": null
        },
        "customVariables": [],
        "customDimensions": [
          {
            "index": "1",
            "value": "Vests"
          }
        ],
        "customMetrics": [],
        "type": "PAGE"
      },
      {
        "hitNumber": "7",
        "time": "23938",
        "hour": "9",
        "minute": "36",
        "isSecure": null,
        "isInteraction": null,
        "referer": null,
        "page": {
          "pagePath": "/vests/yellow.html",
          "hostname": "londoncyclehelmet.com",
          "pageTitle": "London Cycle Helmet - Vests - Yellow",
          "searchKeyword": null,
          "searchCategory": null
        },
        "transaction": {
          "transactionId": null,
          "transactionRevenue": null,
          "transactionTax": null,
          "transactionShipping": null,
          "affiliation": null,
          "currencyCode": null,
          "localTransactionRevenue": null,
          "localTransactionTax": null,
          "localTransactionShipping": null
        },
        "item": {
          "transactionId": null,
          "productName": null,
          "productCategory": null,
          "productSku": null,
          "itemQuantity": null,
          "itemRevenue": null,
          "currencyCode": null,
          "localItemRevenue": null
        },
        "contentInfo": {
          "contentDescription": null
        },
        "appInfo": {
          "name": null,
          "version": null,
          "id": null,
          "installerId": null
        },
        "exceptionInfo": {
          "description": null,
          "isFatal": "false"
        },
        "eventInfo": {
          "eventCategory": "Basket",
          "eventAction": "Add",
          "eventLabel": "Yellow Vest",
          "eventValue": null
        },
        "customVariables": [],
        "customDimensions": [
          {
            "index": "1",
            "value": "Vests"
          }
        ],
        "customMetrics": [],
        "type": "EVENT"
      },
      {
        "hitNumber": "8",
        "time": "0",
        "hour": "9",
        "minute": "44",
        "isSecure": null,
        "isInteraction": null,
        "referer": null,
        "page": {
          "pagePath": "/",
          "hostname": "londoncyclehelmet.com",
          "pageTitle": "London Cycle Helmet",
          "searchKeyword": null,
          "searchCategory": null
        },
        "transaction": {
          "transactionId": null,
          "transactionRevenue": null,
          "transactionTax": null,
          "transactionShipping": null,
          "affiliation": null,
          "currencyCode": null,
          "localTransactionRevenue": null,
          "localTransactionTax": null,
          "localTransactionShipping": null
        },
        "item": {
          "transactionId": null,
          "productName": null,
          "productCategory": null,
          "productSku": null,
          "itemQuantity": null,
          "itemRevenue": null,
          "currencyCode": null,
          "localItemRevenue": null
        },
        "contentInfo": {
          "contentDescription": null
        },
        "appInfo": {
          "name": null,
          "version": null,
          "id": null,
          "installerId": null
        },
        "exceptionInfo": {
          "description": null,
          "isFatal": "false"
        },
        "eventInfo": {
          "eventCategory": null,
          "eventAction": null,
          "eventLabel": null,
          "eventValue": null
        },
        "customVariables": [],
        "customDimensions": [],
        "customMetrics": [],
        "type": "PAGE"
      }
    ],
    "fullVisitorId": "380066991751227408"
  }
]

こんな大きく複雑なデータを世界中のサイトのアクセスごとに保存しているGoogleのデータ力(でーたぢから)は莫大すぎて想像ができない。


で、月100万円もアクセス解析に使えないし、こんな複雑なデータもいらないけど、アクセスデータをBigQueryに入れたくて現在開発&運用中なのが、私が一人プロジェクトで作っているoceanus。

GitHub - uyamazak/oceanus: Save all data to Google BigQuery. Fast and row cost using Docker and Kubernetes on GCP

単純にGETやPOSTリクエストで受け取ったパラメーターをバリデーションして、BigQueryにストリーミングインサートで流し込むことができる。

リクエストは下記のような自作のjsビーコンや、フォーム、imgタグなどを利用しても送ることが出来るし、他のアプリからはhttpsでリクエストはすればいい。


https://www.bizocean.jp/oceanus/okeanides.js

WEBサーバー(apache、nginxとか)のログでもいいかもしれないが、サーバー側にfluentdを入れたりするのは大変だし、サイト上のjsから取れる情報の方が豊富なので、ビーコン型にしている。


一つのoceanusで複数の「サイト」という単位で、BigQueryのテーブルやスキーマを分けたり、RedisのPubSubや、Google Cloud PubSub経由でデータを受け取ることができる。

運用コストは月間1000万PV程度の自社のbizoceanで、ページビューやフォーム、クリックイベント等を取得しているが、Googleに支払うサーバー代などは全部合わせても2万円/月程度で賄うことができている。

今後、機械学習用にBigQueryのスキャン料金が増えてきても5万円程度だろうと推測している。

サーバーはDocker,KubernetesのGKEを利用しているのでデプロイや運用もかなり楽。稼働して10ヶ月なるが、ほぼデータ損失は起こっていない。

現在使っていBigQueryのスキーマは下記のようなもので、1行1イベントにしている。sessionはクッキーに入れて保存している。シンプルなので学習コストが低い。

dt	STRING	REQUIRED  datetime with micro seconds
oid	STRING	REQUIRED  option id or oceanus id
sid	STRING	REQUIRED  session id
uid	STRING	NULLABLE  user id
rad	STRING	REQUIRED  remote address, ip
evt	STRING	REQUIRED  event name
tit	STRING	NULLABLE  title, page title etc
url	STRING	NULLABLE  url
ref	STRING	NULLABLE  referer
jsn	STRING	NULLABLE  json format text
ua	STRING	NULLABLE  user agent
dev	STRING	NULLABLE  device detected by ua
enc	STRING	NULLABLE  encode
scr	STRING	NULLABLE  screen size
vie	STRING	NULLABLE  view size

便利なのがjson型で、例えば検索時に見つからなかったキーワードを取りたいな、とかそういう時にぶち込むことができる。これのお陰でテーブルスキーマは複雑にならずに済む。


oceanusは一応オープンソースだけど、中身にはかなりbizocean用のものが多く入っており、そのまま使うことはできない。使ってみたいという人がいれば簡単な質問ベースであれば無料で出来る範囲で、自社用に立てて欲しい、カスタマイズしてほしいであればコンサルティング的に受けることができるので、自社サイトのデータをBigQueryに入れたいけどどうしたらいいか悩んでいる人はコメントください。

ビッグデータの収集や利用はGCPをはじめ、どんどん手軽になってきている。今後、oceanusのような中小規模向けのアプリケーションがどんどん出てくると思う。