4 ポイント 投稿者 GN⁺ 2023-11-03 | 1件のコメント | WhatsAppで共有
  • Bear Blogは、速度・効率・安定性の制約から、クライアントJavaScriptなしで動作する独自の分析システムを実装した
  • 一般的な分析スクリプトは広告ブロッカーに遮断されることがあり、サーバーログだけではクローラー・スクレイパー・GPTベースのパーサーまで混ざって訪問統計が歪む可能性がある
  • 各ページのCSSでbody:hoverが発生すると、border-image/hit/{{ post.id }}/エンドポイントを呼び出し、hoverやモバイルスクロールを閲覧シグナルとして使う
  • サーバーはuser-agentでボットかどうか・ブラウザー・プラットフォームを確認し、IPアドレスは国判定にのみ使ったうえで、IP+日付ハッシュで重複閲覧を除去する
  • 同じIPから同じ日に複数デバイスで読んでも1回として数えられるが、識別情報を保存せずにページごとのユニーク閲覧数をシンプルに集計できる

CSS hoverで閲覧イベントを作る

  • Bear Blogの分析システムは、クライアント側のJavaScriptを使わない制約に従っている
  • 一般的な分析ツールはクライアントJavaScriptでトラフィックの真正性やボットかどうかをある程度判断できるが、多くの広告ブロッカーはGoogle Analyticsだけでなく、FathomやPlausibleのような分析スクリプトも遮断する
  • サーバーログの解析だけでは、検索エンジンクローラー、スクレイパー、GPTベースのパーサーが通常トラフィックに混ざり、歪んだ見方が生じうる
  • Bearは各ページに次のCSSを入れ、ユーザーがページ上にカーソルを置いたり、モバイルでスクロールしたりしたときにbody:hoverが発生するようにしている
body:hover {
  border-image: url("/hit/{{ post.id }}/?ref={{ request.META.HTTP_REFERER }}");
}
  • body:hoverがトリガーされると、その記事のhit URLが呼び出される
  • このリクエストで明示的に追加している情報はreferrerであり、HTTP_REFERERという綴りは誤綴りのまま標準のように残ったものである
  • ボットはhoverしないという点を利用し、body:hoverベースの呼び出しを人間の読者のシグナルとしている
  • その後サーバー側でuser-agentがボットかどうかを確認し、user-agent文字列からブラウザーとプラットフォームを抽出する

識別情報なしで重複閲覧を除去する

  • 2つ目の制約は、読者を識別できる情報をブラウザークッキーやサーバーに保存しないことだった
  • IPアドレスは国を判断するためにのみ使用し、保存前にはIPアドレスと日付を一緒にハッシュ化する
  • 同じページへの後続リクエストはIPアドレス + 日付ハッシュと比較し、重複なら破棄する
  • この方式では、1日の間に1つのIPアドレスは1ページにつき1回のreadとして計算される
  • IPアドレスの生データは保存せず、日付を含むハッシュには日単位で期限切れになる効果がある
user_agent = httpagentparser.detect(self.request.META.get('HTTP_USER_AGENT', None))
if user_agent.get('bot', False):
    print('Bot traffic')
    return

ip_hash = hashlib.md5(f"{client_ip(self.request)}-{timezone.now().date()}".encode('utf-8')).hexdigest()
country = get_user_location(client_ip(self.request)).get('country_name', '')
device = user_agent.get('platform', {}).get('name', '')
browser = user_agent.get('browser', {}).get('name', '')
referrer = self.request.GET.get('ref', '')

if referrer:
    referrer = urlparse(referrer)
    referrer = '{uri.scheme}://{uri.netloc}/'.format(uri=referrer)

Hit.objects.get_or_create(
    post_id=self.pk,
    ip_address=ip_hash,
    referrer=referrer,
    country=country,
    device=device,
    browser=browser)
  • IPハッシュは1日の重複hitを防ぐ用途にのみ使われ、すべてのページ閲覧は基本的にユニークとして処理される
  • 毎日の終了時にバックグラウンドジョブがhitログからハッシュを削除し、過度に厳格なGDPR解釈と衝突しないようにしている
  • 同じIPアドレスから同じ日に複数デバイスで読んでも1回のreadとしてしか扱われない点は、この方式の欠点である
  • Bearは、このようなケースはトラフィックのごく一部だと見ており、CSSベースの方式は複数の分析収集手法よりも簡潔でシンプルでありながら、より正確な閲覧数を提供すると考えている

1件のコメント

 
GN⁺ 2023-11-03
Hacker News の意見
  • 作者です。IPアドレスのハッシュは、この文脈では1日の中で重複閲覧を防ぐためだけのものです
    各ページビューを基本的に一意にするためのもので、毎日の終わりにワーカーのジョブが閲覧情報は残しつつハッシュを空にします。記事にも明確にするため修正を追加しました

    • 深夜に期限切れになるプライベートキャッシュヘッダーを付けた小さな透明画像を実際に配信し、IPは一切保存しない方法も検討したのか気になります
    • 世界中の共有VPNでユーザー10人が同じIPを使ってサイトに入ってきたら、1人としてしか数えないのか? 会社のネットワークのような場合もあるし、IPは悪い指標です
  • CSSで発生するリクエストを分析に使うというアイデアを初めて見たとき、本当にすごいと思いました
    Twitterのある人が、ページ上に見えない四角形のグリッドを敷き、各マスでhoverすると固有の背景画像が読み込まれるようにしてマウス追跡に使っていました。各背景画像がサーバーに特定のリクエストを送り、サーバーがそれを解釈する仕組みです
    ある夏に遊びでこのアイデアを拡張し、JavaScriptなしの「CSS専用非同期Webチャット」を作りました: https://github.com/kkuchta/css-only-chat

  • 日付とIPだけをハッシュしてIPアドレスを匿名化するというのは、単なるセキュリティ劇場です
    暗号学的ハッシュは高速に計算できるよう設計されています。hashcatを使うとMacBook M1 Proで毎秒60億個のMD5ハッシュを計算でき、IPv4アドレスは40億個しかありません。全範囲を総当たりしてIPアドレスを見つけられるので、実質的にはハッシュを元に戻すのと同じです
    壊れたMD5の代わりにSHA-256のような安全なハッシュを使っても同じです

    • ハッシュからIPを取り戻すのが技術的に些細だという点とは別に、EUのデータ保護機関は個人情報をハッシュしても匿名化ではないと非常に明確にしています
      誰かのフルネームをハッシュしても、あとで「このハッシュはこの特定のフルネームと一致するか」という質問に答えられます。この質問に答えられるということは、匿名化プロセスが可逆であるという意味です
    • 作者です。下にも書きましたが、このスレッドのほうがより関連していそうです
      IPアドレスのハッシュは、この文脈では1日の中で重複閲覧を防ぐためだけのものです。各ページビューを基本的に一意にするためのもので、毎日の終わりにワーカーのジョブが、もう不要になったIPハッシュを削除します
    • 参考までに、Storybookがテレメトリで似たようなことをしていた議論でも同じ問題が出ており[0]、何の最適化もしなくても私の家庭用ノートPCで全IPv4に対するソルト付きハッシュを計算するのに2時間ほどかかりました
      [0] https://news.ycombinator.com/item?id=37596757
    • ハッシュにはソルトを付けるべきです。ソルトを使えば大丈夫で、使わなければ大丈夫ではありません
      ソルトを永続保存するか、定期的にローテーションするかといったことは実装の詳細にすぎず、分析用ハッシュにソルトを付けるときの核心は、ソルトが絶対にクライアントの外に出ないという点です
      記事の説明ではソルトがないように見えます。あるいは現在の日付をソルトのように使っているようですが、これはランダムなソルトではないので、「IP x.y.z.w が yy-mm-dd の日に訪問したか?」を知りたい人なら誰でも簡単に推測できます
      攻撃者の視点で見ると、こうした問題は判断しやすいです。与えられたデータで特定の人物について何かを知るにはどうするか? それができないなら、そのデータは保存しても概ね問題ない可能性が高いです
    • 秘密のソルトやローテーションするソルトを使うこともできるのでは? サンプルコードにはないので、妥当な指摘だとは思います。それでも1つ追加するだけで、かなり合理的に安全にできます
      ただ、こうしたセキュリティ劇場だけでも、プライバシー関連のさまざまな法律や規制を通過するには十分なのではないかと心配です
  • 賢く見えはしますが、body:hoverキーボード専用ユーザーやポインティングデバイスを使わないユーザーエージェント、つまり支援技術のユーザーをほぼ完全に取りこぼす可能性が高いです
    こうした集団が周辺的かもしれないとしても、何らかの形で排除される様子を見るのは常に非常に悪い兆候です
    基本的なCSSだけで、すべてのユーザーエージェントにおいて「実際のユーザーがこの記事を読んでいる」ことを100%信頼できる形で検出し、HTTPリクエストを送る方法があるのかは分かりませんし、疑わしくもあります。一部はCSSをまったくサポートしていないか、CSSの装飾用画像の読み込みをオフにしている可能性もあります
    役立つ可能性のある現代的なセレクターとしては:root:focus-withinがありますが、ユーザーが実際にインタラクティブ要素にフォーカスする必要があり、すべてのユーザーエージェントがそれを保証するわけでもありません。最先端のスクロール連動アニメーションである@scroll-timelineも可能でしょうが、点字リーダーは依然として漏れる可能性があります

    • 周辺的だって? :hoverをサポートしないスマートフォンやタブレット、つまりユーザーエージェントの50%以上に影響するのでは? もちろんマウスを接続していない場合です
    • 書かれている方式のままだと、ポインターのあるデバイスではポインター位置に依存します。ポインターが中央の760px領域内にあれば有効になりますが、外にあると有効になりません
      この領域はコンテンツカラムと左右20pxのパディングを含む幅です。そのため、一部のキーボードユーザーは捕捉され、一部のマウスユーザー、特に大きなビューポートを使うユーザーは捕捉されないことになります
  • 「Google Analyticsのような悪いものだけでなく、FathomやPlausibleも広告ブロックブラウザではアクティビティの記録に苦労する」というのは、彼らが事実上有毒な荒野で生きようとしているからだと思う
    私たちのようなユーザーはその考え方全体にうんざりしていて、だからCSS分析が人気になれば、それも回避しようとする試みが出てくると思う

    • なぜそうなのか?
      私はuBlockでPiwik/Matomo、Plausible、Fathomを手動でブロック解除している。これらが何をどう追跡するのかに害があるとは思わない。そしてサイト運営者に「サービス改善」のための有用な情報を与える
      たとえばPlausibleは、一般的なnginxやApacheのログよりも私について収集する情報が少ない。ブロガーにとっては、記事がHNに載ったのか、どこかからリンクされたのか、どんなコンテンツが価値あるものとして受け止められ、どんなものが無視されているのかを見ることが重要だ。そうしてこそ、実際に読みたいと思われる内容を書き、実際に届くチャネルで広められる
    • Webサーバーのaccess.logを分析サービスに入れても、何も防げない
      むしろユーザーエージェントだけを見てすべてのボットトラフィックを除外するのは事実上不可能なので、数字が膨らむ可能性がある
    • 私にとってWebが有毒な荒野のように感じられるのは、ありとあらゆる広告だ。追跡はずっと微妙な問題だが、長期的には私を操る最適な方法を見つけるために実験可能なデジタルツインが作られ得る、という害がある
      実際にどれほど多くの人がこれを恐れているのかは分からない。「そうだ、気味が悪い」から「あり得ない、ただのSFだ」まで反応は分かれそうだ
    • CSSの読み込みまでブロックできたuMatrixを思い出す
    • この方式はJavaScript方式よりブロックが難しいわけではない。結局、特定のURLパターンへのリクエストを止めるだけだ
  • これは何十年も前からピクセルトラッカーとして知られている

    • メールでも使われている。1x1の透明画像を読み込むほうがhoverイベントをトリガーするより確実だが、広告ブロッカーはそうした画像をよくブロックする
    • その通り。ただしCSSでやると、いくつか興味深い点がある。:hoverを使うと、完全なWebDriverを使っていないボット、つまり大半のボットを除外できる
      ある意味では、空に近い.cssファイルをsupportsとともに@importで読み込む方式のほうがよいかもしれない。広告ブロッカーは1pxの透明トラッキングピクセルは非常によく見つけるが、レイアウトを壊さないように.cssファイルはあまりブロックしない可能性がある。ただしこの場合、:hoverの賢い利点は失われる
  • 純粋に気になる質問なのだが、突き放した意見のように読まれないか心配している。Bearblogのように見える非商用の個人ブログで分析データを収集する目的は何なのか?

    • 2000年頃から継続的にブログを書いてきて、その間ずっと「統計」に非常に関心を持っていた書き手の視点からなら話せる
      分析に関心を持つ主な理由は、文章が読まれているかを見るためだ。表面的には、またある面では虚栄心のためだが、実際には書き手と読者のつながりに関わることだ。読者が何に反応するのかを本気で知りたいし、そういうものをもっと提供したい。その「そういうもの」はテーマかもしれないし、口調かもしれないし、長さかもしれない。読者に合わせて題材を磨く助けになる。結局、私は12のテーマを24通りの方法で書ける。もちろん自分の好きなものを書くが、読者により響くように磨く
      この意味で、分析は読者を知る方法でもある。エンゲージメントの高かったブログでは、分析によって読者についてのぼんやりした人物像のようなものが得られた。何を好むかだけでなく、いつ好むかも見られた。朝一番に読むのか、昼休みに読むのか、深夜に読むのかが分かり、特定の時間に投稿するかを決めたり、その判断に自信を持ったりする助けになった。もちろんすべて曖昧な情報だったが、読者とより積極的につながるのに本当に役立った
    • フィードバックループのためだ。多くの人が考えるのとは違い、分析は広告やデータ販売だけのためのものではなく、サイトやコンテンツの成果を分析することだ
      もちろん広告に使われることもあり、悪用されることもあるが、自分のやっていることへのフィードバックが欲しいなら不可欠だ
      サイトを12人が読もうが12,000人が読もうが、金銭的価値はないかもしれない。しかし個人的な観点では、人々が自分から何を読みたいと思っているのかを知るのはよいことだし、執筆に費やした時間が有意義だったと感じられる。望むなら、より人気のある方向に調整することもできる
    • 好奇心ではないだろうか? 自分が書いたものを誰かが読んでいるのか知りたい。人々が何に関心を持っているのかを知るのも有用だ
      個人ブロガーでも、読者に合わせてコンテンツを調整したいことはある。あるテーマの記事は500人が読み、別のテーマの記事は3人しか読まなかったと知るのはよいことだ
  • 今年の初めにこういうものを作ろうとしたが、Web UIを作っている途中でやる気を失った。私の方式はCSSではなく、単純にタグで偽の画像を読み込むものだ
    https://github.com/nolytics

  • なぜ単にHTTPサーバーからこの情報を得ないのか?

    • ブログ記事ではこう説明している
      「サーバーログをパースする選択肢は常にあり、サーバーにアクセスするトラフィックの種類を大まかに把握できる。しかし一般的に、すべてのサーバートラフィックは同じように見える。技術的にはボットは自分をボットとして識別するユーザーエージェントを持つべきだが、ブラウザを使う『人間』のように情報をかき集めようとするため、そう識別するケースはほとんどない。本質的にサーバーログだけを分析に使うと、検索エンジンのクローラー、スクレイパー、今ではGPTベースのパーサーまで多く、トラフィックを歪めて見ることになる」
    • ボットが全部混ざる
    • サーバーレスで運用していると難しい
  • 分析データはどのように保存するのか?
    ECサイトがあり、販売したい商品があるとしよう。分析以外にも、ログイン状態で商品詳細ページを訪問したといった一部の行動を直接ログとして残すことにした。そのため、ユーザーID、商品ID、タイムスタンプのようなものを保存したい
    実際にはどのように保存すべきなのか? 素朴にはテーブルに入れればよいと思っていた。DBAにデータがどれくらい長く必要かと聞かれ、最低1か月と答えた。すると了解と言われ、おそらくより古いデータを別のテーブルへ移すジョブを設定したようだった
    実際にはこのようなログをどう保存し、どれくらい長く保管するのか?

    • ものすごい規模でなければ、Postgresのテーブルに入れてもまったく問題ない。規模が大きくても、日付や適切なほかの属性を基準にテーブルをパーティショニングして、巨大なインデックスを扱わずに済ませることができる
      以前この方法を試したことがあり、10億行程度になるまではパーティショニングを考える必要すらなかった。それでも、それより早めにパーティショニングするほうがよい。その経験は楽しいものではなかった
    • 分析用データベースのほうがよい。ClickHouseやBigQueryのようなものだ
      集計をはるかに高速に行え、まばらに存在する多数のカラムもうまく扱える。たとえば、paidイベントにはamount属性があり、page_viewイベントにはurl属性がある、という具合だ
    • MySQLに13年分のデータを保存している。年間500万人の訪問者規模だ。そこでクエリを実行するのはつらいので、ClickHouseにもコピーを保持している。ClickHouseはクエリが本当に楽だ
    • PostgresとTimescaleDBを使っている。ECサイトがamazon.com規模でなければうまく動く
      TimescaleDBのよい点は、1時間あたりの商品閲覧数のような関心のある集計について、マテリアライズドビューの作成を自動で処理してくれることだ。イベントが多く、データベースが大きくなりすぎるのを避けたい場合は、イベント自体は「捨てて」集計だけを残すように選ぶこともできる
    • ClickHouse