0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GA4のシンガポール流入はボットか? ASNなしで見分ける2つのチェック(画面解像度とChromeバージョン)

0
Posted at

GA4 の国別レポートで、シンガポールからの Direct 流入が急に増えることがあります。自分のサイトでは28日で210セッション、エンゲージは1件、滞在は0秒でした。

人ではなさそうだと分かっても、確かめる手段が意外と限られます。アクセス元のネットワーク(ASN)を見ればデータセンターかどうかは一発ですが、Cloudflare の無料プランでは API から ASN が取れません。

ここでは、ASN を使わずに「人かボットか」を判定するために使った2つのチェックを、そのまま動くコードつきでまとめます。

チェック1: GA4 で画面解像度を見る

一番効いたのは画面解像度でした。シンガポールの210セッションのうち201件が 1280x1200 を報告していました。

本物の訪問者なら解像度は何十通りにも散らばります。1つの国のほぼ全セッションが同じ珍しい値なら、それはツールの設定値です。

GA4 Data API(Python)で、国を絞って screenResolution 別に出します。

from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
    RunReportRequest, Dimension, Metric, DateRange, FilterExpression, Filter,
)

def by_resolution(client, property_id, country="Singapore", days=28):
    req = RunReportRequest(
        property=f"properties/{property_id}",
        dimensions=[Dimension(name="screenResolution")],
        metrics=[Metric(name="sessions"), Metric(name="engagedSessions")],
        date_ranges=[DateRange(start_date=f"{days}daysAgo", end_date="today")],
        dimension_filter=FilterExpression(filter=Filter(
            field_name="country",
            string_filter=Filter.StringFilter(value=country),
        )),
    )
    for row in client.run_report(req).rows:
        print(row.dimension_values[0].value,
              [m.value for m in row.metric_values])

管理画面でやるなら、探索レポートで「国」と「画面の解像度」をディメンションに並べるだけで同じものが見られます。browser と operatingSystem も並べると、ボットは「Chrome / Windows」に固まって出ます。

1280x1200 という値は今回のボットの設定なので、別のサイトでは別の値かもしれません。見るべきは値そのものより「1つの値に偏っているか」です。

チェック2: Cloudflare のビーコンで Chrome のバージョンを見る

もう1つは、JavaScript を実行したクライアントが名乗る Chrome のバージョンです。

Cloudflare Web Analytics を入れているサイトでは、ページの JavaScript が /cdn-cgi/rum にビーコンを送ります。このパスを叩くのは JavaScript を実行したクライアントだけなので、GA4 のタグを発火させる相手と同じです。無料プランでも、このパスを User-Agent 別に数えるところまではできます。

import re, collections, datetime as dt, requests

GQL = "https://api.cloudflare.com/client/v4/graphql"
QUERY = """
query($zone: String!, $since: Time!, $until: Time!, $country: String!) {
  viewer { zones(filter: { zoneTag: $zone }) {
    httpRequestsAdaptiveGroups(
      limit: 5000,
      filter: {
        datetime_geq: $since, datetime_lt: $until,
        clientCountryName: $country,
        clientRequestPath: "/cdn-cgi/rum"
      }
    ) { count dimensions { userAgent } }
  } }
}
"""

def chrome_versions(token, zone, country, days=7):
    now = dt.datetime.now(dt.timezone.utc).replace(microsecond=0)
    ver = collections.Counter()
    for i in range(days):  # 無料プランは1クエリ1日まで
        until = now - dt.timedelta(days=i)
        since = until - dt.timedelta(days=1)
        r = requests.post(GQL, headers={"Authorization": f"Bearer {token}"}, json={
            "query": QUERY,
            "variables": {"zone": zone, "country": country,
                          "since": since.isoformat().replace("+00:00", "Z"),
                          "until": until.isoformat().replace("+00:00", "Z")},
        }, timeout=60).json()
        for g in r["data"]["viewer"]["zones"][0]["httpRequestsAdaptiveGroups"]:
            m = re.search(r"Chrome/(\d+)", g["dimensions"]["userAgent"])
            ver[m.group(1) if m else "other"] += g["count"]
    return ver.most_common()

print("SG", chrome_versions(TOKEN, ZONE, "SG"))
print("JP", chrome_versions(TOKEN, ZONE, "JP"))  # 比較対象

ポイントは、自分の読者がいる国を比較対象として一緒に流すことです。自分のサイトではこうなりました。

シンガポール 日本(比較対象)
Chrome のバージョン 103〜133、メジャー版で十数種類 153 と 154
OS すべて Windows Windows
ビーコンが来た時間帯(UTC) 24時間中23時間 数時間に固まる

Chrome は自動更新されるので、本物の読者は最新版(2026年10月時点で153〜154)に揃います。同じ都市から古いバージョンが満遍なく散らばって来るのは、ツールが User-Agent をリストから選んで名乗っている形です。

時間帯も見ておくと確度が上がります。夜の落ち込みが無く、24時間ほぼ均等に来ているなら人ではありません。

ハマりどころ: 無料プランで clientAsn を指定するとエラー

最初は ASN を見ようとして、clientAsn を指定しました。無料プランのゾーンではこう返ります。

zone '...' does not have access to the field 'clientasndescription'

クエリ全体が失敗するので、ASN 系のフィールドは外してください。User-Agent・パス・ステータス・時間帯(datetimeHour)は無料プランでも取れます。

判定の目安

4つ揃えば、ASN が無くてもボットと言い切ってよいと思います。

観点 ボット側 人間側
エンゲージ 0秒・1ページで離脱が大半 スクロール・滞在がある
画面解像度 1つの値に偏る 何十通りに散らばる
Chrome バージョン 古い版が満遍なく 最新版に揃う
時間帯 24時間ほぼ均等 生活時間に偏る

GA4 は既知のボットを自動で除外しますが、そのリストは見ることも追加することもできません。普通のブラウザを名乗るヘッドレス Chrome はそこをすり抜けるので、除外は分析する側でやることになります。


ボットが実際にどのページを巡回していて、除外したら「よく読まれている記事」の顔ぶれがどう変わったか、国ごと遮断しなかった理由は Aulvem 本家にまとめました → Aulvem|GA4の国別1位はシンガポールのボットだった — ASNが見えないCloudflare無料プランで見分けた手順

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?