| 状況 | やること |
|---|---|
| GA4 インポートCV と既存CV が同じ行動を二重に数えている | お金に近い 1 つだけ集計オン、他は集計オフ(削除しない) |
| 自動入札の成果が急に「良く見える」のに売上が増えない | 二重計上を疑い、集計対象を 1 行動 1 つに整理 |
| 一度オフにしたが履歴を見たい | オフでも記録は残る。レポートで参照可 |
google.ads.googleads.errors.GoogleAdsException:
status = StatusCode.INVALID_ARGUMENT
details = "Request contains an invalid argument."
message: "The error code is not in this version."
trigger { int64_value: 4 }
field_path_elements { field_name: "operations" index: 0 }
{ field_name: "update" }
{ field_name: "status" }
「The error code is not in this version」── このエラーメッセージは、 API v24 の仕様の隙間を突いた瞬間にだけ顔を出します。本記事では、google-ads Python SDK で Conversion Action を mutate しようとして踏んだ 5 つの罠 と、それぞれの突破コードを記録します。
背景 — なぜ HIDDEN に戻したかったか
クライアントの Google Ads アカウントで、 からインポートした Conversion Action 3 件を一度 ENABLED に戻しました。GA4 側で発火しているリードシグナルを Google Ads でも見えるようにするためです。 しかし管理画面で確認したところ、既存の WEBPAGE 系 Conversion Action(モデルハウス見学予約・電話クリック等)と 同じユーザー行動で重複計上される可能性 に気づきました。 | Conversion Action | type | category | 既存 incl_metric | 私が ENABLED 化 | |---|---|---|:-:|:-:| | モデルハウス見学予約_TESLA | WEBPAGE | SUBMIT_LEAD_FORM | True | — | | 電話クリック_LP | WEBPAGE | PHONE_CALL_LEAD | True | — | | ランディ申し込み完了 (GA4 import) | GA4_CUSTOM | DEFAULT | False | ENABLED 化 | | phone_click (GA4 import) | GA4_CUSTOM | PHONE_CALL_LEAD | False | ENABLED 化 | | click_to_call (GA4 import) | GA4_CUSTOM | PHONE_CALL_LEAD | False | ENABLED 化 | 電話タップ 1 回で電話クリック_LP + phone_click + click_to_call の 3 倍計上 になる懸念があり、慌てて HIDDEN に戻すスクリプトを書きました。それが冒頭のエラーに繋がります。
罠 1: ENABLED → HIDDEN は API から mutate できない
最初に書いた mutation コードはこうでした。op = client.get_type("ConversionActionOperation")
op.update.resource_name = ca_service.conversion_action_path(cid, conversion_action_id)
op.update.status = client.enums.ConversionActionStatusEnum.HIDDEN
op.update_mask.paths.append("status")
resp = ca_service.mutate_conversion_actions(
customer_id=cid, operations=[op]
)
シンプルです。何が問題か分かりません。
エラーログをよく読むと、ヒントが隠れていました。
trigger { int64_value: 4 }
field_path_elements { field_name: "status" }
int64_value: 4 は status enum の 4 番目の値 を意味します。ConversionActionStatusEnum のソースを引くと、4 番目は HIDDEN です。
ConversionActionStatusEnum:
0: UNSPECIFIED
1: UNKNOWN
2: ENABLED
3: REMOVED
4: HIDDEN
つまり API は 「
statusフィールドにHIDDENという値を入れる mutation は、このバージョンでは許可されていない」 と返してきていたわけです。
💡 KEY TAKEAWAYS
Google Ads API のConversionActionStatusEnum.HIDDENは UI からのみ設定可能な内部状態 として設計されており、API 経由の mutation では拒否されます。同様の制約は他の enum でも存在するので、The error code is not in this versionを見たら、まずtrigger.int64_valueから enum 値を逆引きします。
突破方法: include_in_conversions_metric=False で集計から外す
集計から外すという目的だけなら、include_in_conversions_metricフィールドを False にすれば達成できます。こちらは API で mutate 可能です。
op = client.get_type("ConversionActionOperation")
op.update.resource_name = ca_service.conversion_action_path(cid, conversion_action_id)
op.update.include_in_conversions_metric = False
op.update_mask.paths.append("include_in_conversions_metric")
resp = ca_service.mutate_conversion_actions(
customer_id=cid, operations=[op]
)
| incl_metric | primary_for_goal | 自動入札への影響 | 集計列 |
|---|---|---|---|
| True | True | 影響大、★主要 CV | Conversions / All Conv |
| True | False | 影響あり(集計に入る) | Conversions / All Conv |
| False | True | 無し(マイクロ CV 扱い) | All Conv のみ |
| False | False | 無し(参考値) | All Conv のみ |
client.copy_from(
op.update_mask,
client.get_type("FieldMask")(paths=["status"])
)
これは v22 まで動いていたコードです。v24 では:
ValueError: Specified type 'FieldMask' does not exist in Google Ads API v24
FieldMask という型自体が SDK の type レジストリから削除されました。
突破方法: paths を直接 append する
正しい書き方:
op.update_mask.paths.append("status")
# 複数フィールドの場合
op.update_mask.paths.extend(["status", "name", "value_settings.default_value"])
update_maskは protobuf のFieldMaskメッセージで、pathsは repeated string です。新しい SDK では type を経由せず、operation の field に直接書きます。
💡 KEY TAKEAWAYS
google-ads SDK のメジャーバージョン更新では、こうした 型レジストリの整理 が必ず混じります。エラー文言でdoes not exist in Google Ads API vXXを見たら、リリースノートで該当型を検索します。
罠 3: USER_PERMISSION_DENIED は「別 MCC 配下」のサイン
別の場面で MCC 配下のクライアント一覧を取得しようとしました。
customer_service = client.get_service("CustomerService")
ga_service = client.get_service("GoogleAdsService")
accessible = customer_service.list_accessible_customers()
for resource in accessible.resource_names:
cid = resource.split("/")[-1]
query = "SELECT customer_client.id, customer_client.descriptive_name FROM customer_client"
try:
for batch in ga_service.search_stream(customer_id=cid, query=query):
for row in batch.results:
print(row.customer_client.descriptive_name, row.customer_client.id)
except GoogleAdsException as e:
print(f" {cid}: {extract_error_code(e)}")
list_accessible_customers() で 18 件の top-level customer が返ってきましたが、そのうち 5 件が USER_PERMISSION_DENIED を返しました。
9066450572: authorization_error: USER_PERMISSION_DENIED
8538982978: authorization_error: USER_PERMISSION_DENIED
9482254684: authorization_error: USER_PERMISSION_DENIED
4815331144: authorization_error: USER_PERMISSION_DENIED
9585924613: authorization_error: USER_PERMISSION_DENIED
最初は 「権限がない、招待を受けていない」 と解釈しました。実際は違いました。
突破方法: login_customer_id を変えて再試行
USER_PERMISSION_DENIED は その customer が別 MCC の配下にある サインです。私が現在使っている login_customer_id(自分の MCC)からは見えないだけで、別の MCC を login_customer_id に指定すれば取れます。
# 別 MCC 配下のアカウントを取りに行く
client_for_other_mcc = GoogleAdsClient.load_from_dict({
"developer_token": cfg["developer_token"],
"refresh_token": token_data["refresh_token"],
"client_id": installed["client_id"],
"client_secret": installed["client_secret"],
"login_customer_id": "OTHER_MCC_ID_HERE", # 別 MCC の 10 桁 ID
"use_proto_plus": True,
})
| エラーコード | 意味 | 対処 |
|---|---|---|
USER_PERMISSION_DENIED | 別 MCC 配下にある | login_customer_id を変えて再試行 |
CUSTOMER_NOT_ENABLED | アカウントが停止・解約済み | スキップ可、運用停止アカウント |
DEVELOPER_TOKEN_NOT_APPROVED | Test access のまま | Basic access を申請 |
AUTHENTICATION_ERROR | token 失効 | refresh または再認証 |
# A. search
resp = ga.search(customer_id=cid, query=q)
for row in resp: # ListPager
print(row.campaign.name)
# B. search_stream
stream = ga.search_stream(customer_id=cid, query=q)
for batch in stream: # generator of batches
for row in batch.results:
print(row.campaign.name)
使い分け
| 観点 | search | search_stream | |---|---|---| | 戻り値 | ListPager(自動ページング) | generator(batch 単位) | | total_results | 取れる | 取れない | | メモリ効率 | △(全件保持) | ○(batch 単位で開放) | | 大量データ向き | × | ○ | | 並列処理 | × | ○ | 大量データ取得は search_stream、件数や ListPager 機能が欲しいときは search と覚えるとシンプルです。 私が今回踏んだ罠は、search_stream の戻り値を「list だ」と誤解して len(stream) した瞬間です。search_stream の戻り値は generator なので len() は使えません。for batch in stream で逐次処理してください。
罠 5: REMOVED は不可逆、UI でも復元不可
罠 1 で「HIDDEN がダメなら REMOVED にすればよい」と一瞬考えました。op.update.status = client.enums.ConversionActionStatusEnum.REMOVED
REMOVED は API で許可されています。ところが REMOVED は不可逆 です。一度 REMOVED にした Conversion Action は、UI でも API でも ENABLED に戻せません。
REMOVED したい本当の理由が「もう使わない」なら問題ありません。「一時的に集計除外」が目的なら、include_in_conversions_metric=False で運用するのが正解です。
完成形 — 共有の一括追加
罠を全部回避した実用コードを置きます。Tesla キャンペーンに EXACT match の共有除外キーワードを 33 件追加した実例です。
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException
CAMPAIGN_ID = 23806896702
NEGATIVE_KEYWORDS_EXACT = [
"太陽 光",
"太陽 光 発電",
"蓄電池 奈良",
# ... 33 件
]
def add_exact_negatives(client, cid: str, campaign_id: int, keywords: list[str]):
cs = client.get_service("CampaignService")
cc_service = client.get_service("CampaignCriterionService")
operations = []
for kw in keywords:
op = client.get_type("CampaignCriterionOperation")
cc = op.create
cc.campaign = cs.campaign_path(cid, campaign_id)
cc.negative = True
cc.keyword.text = kw
cc.keyword.match_type = client.enums.KeywordMatchTypeEnum.EXACT
operations.append(op)
try:
resp = cc_service.mutate_campaign_criteria(
customer_id=cid, operations=operations
)
return [r.resource_name for r in resp.results]
except GoogleAdsException as e:
for err in e.failure.errors:
print(f" ❌ {err.message}")
raise
- エラーログの
trigger.int64_valueを必ず読む — enum 値の特定にこれが効く field_path_elementsで問題のフィールド位置を特定する — どの operation のどの field が問題か即座に分かる- SDK のメジャーバージョン更新時はリリースノートを必ず確認 — 型レジストリの整理が混じる
- status 系の mutation は不可逆を疑う — REMOVED や HIDDEN は元に戻せないと考える
