Cloud RunからMeasurement Protocolを使ってGA4にUser-IDを送信してみた
概要
GA4(Googleアナリティクス4)のMeasurement Protocolを使うと、サーバーサイドからGA4にイベントデータを送信することができます。
Googleの公式ドキュメントでは、Measurement Protocolのユースケースとして以下が挙げられています。
- オンラインとオフラインでの行動を結び付ける
- クライアントサイドとサーバーサイドの処理を測定する
- 標準的なユーザー操作以外で発生するイベント(オフライン コンバージョンなど)を送信する
- 自動収集ができないデバイスやアプリ(例: キオスク、スマートウォッチ)からイベントを送信する
今回はCloud Run(Python)からMeasurement Protocolを使い、User-IDをGA4に送信する方法を実際に検証してみました。
事前準備
Measurement IDとAPIシークレットの取得
Measurement Protocolを使用するにはMeasurement IDとAPIシークレットが必要です。
Measurement IDは、GA4の測定ID(トラッキングコード)を設定します。G-XXXXXXXXXXの形式です。
API Secretは、同じデータストリームの設定画面で「Measurement Protocol APIシークレット」セクションから作成します。「作成」ボタンをクリックし、ニックネームを入力してシークレットを生成します。
初めて作成する場合は「ユーザーデータ収集の確認」のが出ますので内容を確認して問題なければ【確認しました】を押下します。

その後【作成】を押下してシークレットを作成します。

作成したシークレットを控えたら後続フェーズに進みます。
Client IDについて
Measurement Protocolのペイロードにはclient_idフィールドが必須です。ただし、この値は任意のランダムな文字列でも送信自体は問題なく通ります。
client_idの役割は、GA4側でブラウザセッションのデータと紐づけることです。実際の値を指定すれば、そのユーザーの既存のウェブセッションデータとMeasurement Protocolで送信したイベントが統合されます。
一方、ランダムな値を指定した場合はブラウザ側のデータとの紐づけは行われませんが、user_idを指定していればGA4のUser-IDベースのレポートには反映されます。
- 既存のウェブセッションと紐づけたい場合: フロントから値を取得してサーバーに連携する
- サーバーサイドのイベントのみで完結する場合: ランダムな文字列(例: UUID)を指定すればOK
やってみる
ディレクトリ構成
以下のようなディレクトリ構成で作業します。
mp-userid-sender/
├── main.py
└── requirements.txt
requirements.txt
functions-framework==3.*
requests==2.*
main.py
Measurement ProtocolにUser-IDを含めてイベントを送信する関数を作成します。Measurement IDとAPIシークレットはファイル内に直接記載しています。
import functions_framework
import requests
# GA4の設定
GA4_MEASUREMENT_ID = "測定ID(トラッキングコード)"
GA4_API_SECRET = "APIシークレット"
MP_ENDPOINT = "https://www.google-analytics.com/mp/collect"
@functions_framework.http
def send_user_id(request):
"""User-IDを含めたイベントをMeasurement Protocolで送信する"""
req = request.get_json(silent=True)
if not req:
return "リクエストボディが空です", 400
# 必須パラメータ
client_id = req.get("client_id")
user_id = req.get("user_id")
if not client_id or not user_id:
return "client_idとuser_idは必須です", 400
# イベント名(デフォルトはlogin)
event_name = req.get("event_name", "login")
# イベントパラメータ
event_params = req.get("event_params", {})
# engagement_time_msecを含める
event_params.setdefault("engagement_time_msec", "10000")
# Measurement Protocolペイロードの構築
payload = {
"client_id": client_id,
"user_id": user_id,
"events": [
{
"name": event_name,
"params": event_params,
}
],
}
url = f"{MP_ENDPOINT}?measurement_id={GA4_MEASUREMENT_ID}&api_secret={GA4_API_SECRET}"
response = requests.post(url, json=payload)
print(f"Measurement Protocol送信完了: status={response.status_code}, user_id={user_id}")
print(f"ペイロード: {payload}")
return {
"status": "success",
"mp_status_code": response.status_code,
"user_id": user_id,
"event_name": event_name,
}, 200
少し解説します。
client_id = req.get("client_id")
user_id = req.get("user_id")
if not client_id or not user_id:
return "client_idとuser_idは必須です", 400
リクエストボディのJSONからclient_idとuser_idを取得しています。この2つはMeasurement Protocolに送信する上で必要なパラメータのため、どちらかが欠けている場合は400エラーを返すようにしています。
event_params.setdefault("engagement_time_msec", "10000")
engagement_time_msecが指定されていない場合にデフォルト値(10000ms:10秒)を設定しています。このパラメータはエンゲージメント時間をミリ秒単位で指定するもので、GA4がアクティブユーザーやエンゲージメントセッションを判定する際に使用されます。GA4では、セッション内のengagement_time_msecの合計が10秒(10,000ms)を超えるとエンゲージメントセッションとして扱われるようです。
0や短い値を指定するとアクティブユーザーとしてカウントされない場合があるため注意してください。
payload = {
"client_id": client_id,
"user_id": user_id,
"events": [
{
"name": event_name,
"params": event_params,
}
],
}
ペイロードの構築部分では、Measurement Protocolの仕様に従ってJSON構造を組み立てています。client_idとuser_idはトップレベルに、イベント名とパラメータはevents配列の中に配置します。user_idをトップレベルに含めることで、GA4側でUser-IDとしてイベントに紐づけられます。
url = f"{MP_ENDPOINT}?measurement_id={GA4_MEASUREMENT_ID}&api_secret={GA4_API_SECRET}"
response = requests.post(url, json=payload)
送信先URLのクエリパラメータにmeasurement_idとapi_secretを付与してPOSTリクエストを送信します。Measurement Protocolは認証にOAuthなどを使わず、このAPIシークレットのみで認証する仕組みです。
デプロイ
Cloud Run Functionsとしてデプロイします。--function引数にエントリポイントの関数名を指定します。
gcloud run deploy mp-userid-sender \
--source . \
--function=send_user_id \
--base-image python312 \
--region asia-northeast1 \
--no-allow-unauthenticated
デプロイが完了したらコンソールから確認します。Cloud Runのサービス一覧にmp-userid-senderが表示されていれば問題ありません。
動作確認
デプロイした関数を呼び出してUser-IDを送信してみます。まず関数のURLとアクセストークンを取得します。
FUNCTION_URL=$(gcloud run services describe mp-userid-sender --region asia-northeast1 --format='value(status.url)')
TOKEN=$(gcloud auth print-identity-token)
上記認証トークン(TOKEN)は一定時間で無効になるので時間をおいて試す場合は再度実行してください。
curl -X POST $FUNCTION_URL \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"client_id": "123456789.1234567890",
"user_id": "user-001",
"event_name": "login",
"event_params": {
"debug_mode": 1,
"engagement_time_msec": "100",
"session_id": "1234567890"
}
}'
以下のようなレスポンスが返ってきたら成功です。
{"event_name":"login","mp_status_code":204,"status":"success","user_id":"user-001"}
GA4のDebugViewでイベントが届いているか確認します。イベントパラメータにdebug_modeを追加して送信しているので、GA4の「管理」→「データの表示」→「DebugView」でDebugViewを開くと送信したイベントが表示されます。

debug_modeについては以下のリファレンスに記載があります。
DebugViewではイベントがタイムライン上にリアルタイムで表示され、イベントをクリックするとパラメータの詳細も確認できます。「現在アクティブなユーザー プロパティ」セクションにuser_idが表示されていればUser-IDの送信が成功しています。
実際に送信した結果ですが、以下のようにDebugViewに表示されます。

左側のタイムラインにリクエストで指定したイベント名loginイベントが表示され、右下の「現在アクティブなユーザー プロパティ」に指定したuser_id(user-001)の値が確認できます。無事GA4にUser-IDを連携することができ、確認もできました。
開発時はDebugViewを用いた確認方法がおすすめです。
また、Measurement Protocolは成功時にステータスコード204を返し、レスポンスボディは空です。エラーがあっても2xxが返ってくる場合があるため、後述のバリデーションエンドポイントで事前にペイロード内容を確認することも重要です。
バリデーションエンドポイントで事前検証
Measurement Protocolにはバリデーション用のエンドポイントが用意されています。実際にGA4へ送信する前にペイロードの正当性を検証できます。
本番用のエンドポイント/mp/collectは、ペイロードに問題があっても2xxを返してしまいエラー内容がわかりません。一方、バリデーションエンドポイント/debug/mp/collectを使うとレスポンスボディにエラーの詳細が返ってきます。
それでは実際にCloud Run Functionsを経由せず、curlで直接バリデーションエンドポイントにペイロードを送信して試してみます。
正常なリクエスト
まずは正常なペイロードを送信してみます。
curl -X POST "https://www.google-analytics.com/debug/mp/collect?measurement_id=G-QHDFZPCEDT&api_secret=Xjk8qdmbRb6ffvSiaNnMAQ" \
-H "Content-Type: application/json" \
-d '{
"client_id": "123456789.1234567890",
"user_id": "user-001",
"events": [{"name": "login", "params": {"engagement_time_msec": "100"}}]
}'
正常な場合、validationMessagesが空の配列で返ってきます。
{
"validationMessages": []
}
イベント名が不正なリクエスト
イベント名を_bad_eventのようにアンダースコア始まりにしてみます。GA4のイベント名はアルファベットで始まる必要があります。
curl -X POST "https://www.google-analytics.com/debug/mp/collect?measurement_id=G-QHDFZPCEDT&api_secret=Xjk8qdmbRb6ffvSiaNnMAQ" \
-H "Content-Type: application/json" \
-d '{
"client_id": "123456789.1234567890",
"user_id": "user-001",
"events": [{"name": "_bad_event"}]
}'
以下のようにエラーメッセージが返ってきます。
{
"validationMessages": [ {
"fieldPath": "events",
"description": "Event at index: [0] has invalid name [_bad_event]. Names must start with an alphabetic character.",
"validationCode": "NAME_INVALID"
} ]
}
fieldPathでどのフィールドに問題があるか、descriptionでエラーの詳細、validationCodeでエラーの種類がわかりますね。
client_idが空のリクエスト
client_idを空文字にしてみます。
curl -X POST "https://www.google-analytics.com/debug/mp/collect?measurement_id=G-QHDFZPCEDT&api_secret=Xjk8qdmbRb6ffvSiaNnMAQ" \
-H "Content-Type: application/json" \
-d '{
"client_id": "",
"user_id": "user-001",
"events": [{"name": "login"}]
}'
{
"validationMessages": [ {
"fieldPath": "client_id",
"description": "Measurement requires a client_id.",
"validationCode": "VALUE_REQUIRED"
} ]
}
Client_IDは必須のため、validationCodeにVALUE_REQUIREDが設定され返却されています。
リファレンスで確認したところ、ValidationCodeは以下となっています。
| validationCode | 説明 |
|---|---|
| VALUE_INVALID | fieldPathに指定した値が無効でした。 |
| VALUE_REQUIRED | fieldPathに必須の値が指定されていませんでした。 |
| NAME_INVALID | 指定した名前が無効でした。 |
| NAME_RESERVED | 指定した名前は予約済みの名前の1つでした。 |
| VALUE_OUT_OF_BOUNDS | 指定した値が大きすぎました。 |
| EXCEEDED_MAX_ENTITIES | リクエストに含まれるパラメータが多すぎました。 |
| NAME_DUPLICATED | リクエストで同じ名前が複数回指定されていました。 |
まとめ
Cloud RunからMeasurement Protocolを使ってGA4にUser-IDを送信する方法を検証してみました。
ポイントをまとめると以下の通りです。
client_idはペイロードの必須フィールドだが、ランダムな値でもリクエストは通る。既存のウェブセッションと紐づけたい場合は実際の値を使用する- Measurement Protocolは成功時に
204を返すが、エラーでも2xxが返る場合があるため、リクエスト内容はバリデーションエンドポイントでの事前検証が重要 - DebugViewで確認するにはイベントパラメータに
debug_mode: 1を含めて送信する
この仕組みを使うことで、サーバーサイドで管理しているユーザーIDをGA4に連携し、クロスデバイス計測やUser-IDベースのレポートを活用できるようになります。
また指定できるイベント数が25個までなど制限事項があるので注意が必要です。








