|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Google Ads CLI for managing conversions from the terminal.""" |
| 3 | + |
| 4 | +import argparse |
| 5 | +import sys |
| 6 | +from pathlib import Path |
| 7 | + |
| 8 | +from google.ads.googleads.client import GoogleAdsClient |
| 9 | +from google.ads.googleads.errors import GoogleAdsException |
| 10 | + |
| 11 | + |
| 12 | +CONFIG_PATH = Path(__file__).parent / "google-ads.yaml" |
| 13 | + |
| 14 | + |
| 15 | +def get_client(): |
| 16 | + if not CONFIG_PATH.exists(): |
| 17 | + print(f"Error: {CONFIG_PATH} not found.", file=sys.stderr) |
| 18 | + print("Copy google-ads.yaml.example to google-ads.yaml and fill in credentials.", file=sys.stderr) |
| 19 | + sys.exit(1) |
| 20 | + return GoogleAdsClient.load_from_storage(str(CONFIG_PATH)) |
| 21 | + |
| 22 | + |
| 23 | +def list_conversions(args): |
| 24 | + """List all conversion actions for the given customer.""" |
| 25 | + client = get_client() |
| 26 | + ga_service = client.get_service("GoogleAdsService") |
| 27 | + |
| 28 | + query = """ |
| 29 | + SELECT |
| 30 | + conversion_action.id, |
| 31 | + conversion_action.name, |
| 32 | + conversion_action.category, |
| 33 | + conversion_action.type, |
| 34 | + conversion_action.status, |
| 35 | + conversion_action.tag_snippets |
| 36 | + FROM conversion_action |
| 37 | + """ |
| 38 | + |
| 39 | + response = ga_service.search(customer_id=args.customer_id, query=query) |
| 40 | + |
| 41 | + count = 0 |
| 42 | + for row in response: |
| 43 | + ca = row.conversion_action |
| 44 | + print(f"ID: {ca.id}") |
| 45 | + print(f" Name: {ca.name}") |
| 46 | + print(f" Category: {ca.category.name}") |
| 47 | + print(f" Type: {ca.type_.name}") |
| 48 | + print(f" Status: {ca.status.name}") |
| 49 | + if ca.tag_snippets: |
| 50 | + for snippet in ca.tag_snippets: |
| 51 | + print(f" Tag type: {snippet.type_.name}") |
| 52 | + print(f" Snippet: {snippet.event_snippet}") |
| 53 | + print() |
| 54 | + count += 1 |
| 55 | + |
| 56 | + print(f"Total: {count} conversion action(s)") |
| 57 | + |
| 58 | + |
| 59 | +def create_conversion(args): |
| 60 | + """Create a new conversion action.""" |
| 61 | + client = get_client() |
| 62 | + conversion_action_service = client.get_service("ConversionActionService") |
| 63 | + |
| 64 | + operation = client.get_type("ConversionActionOperation") |
| 65 | + action = operation.create |
| 66 | + |
| 67 | + action.name = args.name |
| 68 | + action.category = getattr( |
| 69 | + client.enums.ConversionActionCategoryEnum.ConversionActionCategory, |
| 70 | + args.category, |
| 71 | + ) |
| 72 | + action.type_ = getattr( |
| 73 | + client.enums.ConversionActionTypeEnum.ConversionActionType, |
| 74 | + args.type, |
| 75 | + ) |
| 76 | + action.status = client.enums.ConversionActionStatusEnum.ConversionActionStatus.ENABLED |
| 77 | + |
| 78 | + if args.value is not None: |
| 79 | + action.value_settings.default_value = args.value |
| 80 | + action.value_settings.always_use_default_value = True |
| 81 | + |
| 82 | + response = conversion_action_service.mutate_conversion_actions( |
| 83 | + customer_id=args.customer_id, |
| 84 | + operations=[operation], |
| 85 | + ) |
| 86 | + |
| 87 | + for result in response.results: |
| 88 | + print(f"Created conversion action: {result.resource_name}") |
| 89 | + |
| 90 | + |
| 91 | +def get_tag_snippets(args): |
| 92 | + """Get tag snippets for a conversion action.""" |
| 93 | + client = get_client() |
| 94 | + ga_service = client.get_service("GoogleAdsService") |
| 95 | + |
| 96 | + query = f""" |
| 97 | + SELECT |
| 98 | + conversion_action.id, |
| 99 | + conversion_action.name, |
| 100 | + conversion_action.tag_snippets |
| 101 | + FROM conversion_action |
| 102 | + WHERE conversion_action.id = {args.conversion_action_id} |
| 103 | + """ |
| 104 | + |
| 105 | + response = ga_service.search(customer_id=args.customer_id, query=query) |
| 106 | + |
| 107 | + for row in response: |
| 108 | + ca = row.conversion_action |
| 109 | + print(f"Conversion action: {ca.name} (ID: {ca.id})") |
| 110 | + if ca.tag_snippets: |
| 111 | + for snippet in ca.tag_snippets: |
| 112 | + print(f"\n--- {snippet.type_.name} snippet ---") |
| 113 | + if snippet.global_site_tag: |
| 114 | + print("Global site tag:") |
| 115 | + print(snippet.global_site_tag) |
| 116 | + if snippet.event_snippet: |
| 117 | + print("Event snippet:") |
| 118 | + print(snippet.event_snippet) |
| 119 | + else: |
| 120 | + print("No tag snippets available for this conversion action.") |
| 121 | + |
| 122 | + |
| 123 | +def check_status(args): |
| 124 | + """Check conversion tracking status for the customer.""" |
| 125 | + client = get_client() |
| 126 | + ga_service = client.get_service("GoogleAdsService") |
| 127 | + |
| 128 | + query = """ |
| 129 | + SELECT |
| 130 | + customer.conversion_tracking_setting.conversion_tracking_id, |
| 131 | + customer.conversion_tracking_setting.conversion_tracking_status, |
| 132 | + customer.conversion_tracking_setting.cross_account_conversion_tracking_id, |
| 133 | + customer.conversion_tracking_setting.accepted_customer_data_terms |
| 134 | + FROM customer |
| 135 | + LIMIT 1 |
| 136 | + """ |
| 137 | + |
| 138 | + response = ga_service.search(customer_id=args.customer_id, query=query) |
| 139 | + |
| 140 | + for row in response: |
| 141 | + cts = row.customer.conversion_tracking_setting |
| 142 | + print(f"Conversion Tracking ID: {cts.conversion_tracking_id}") |
| 143 | + print(f"Tracking Status: {cts.conversion_tracking_status.name}") |
| 144 | + print(f"Cross-Account Tracking ID: {cts.cross_account_conversion_tracking_id}") |
| 145 | + print(f"Accepted Customer Data Terms: {cts.accepted_customer_data_terms}") |
| 146 | + |
| 147 | + |
| 148 | +def upload_conversion(args): |
| 149 | + """Upload an offline click conversion.""" |
| 150 | + client = get_client() |
| 151 | + conversion_upload_service = client.get_service("ConversionUploadService") |
| 152 | + conversion_action_service = client.get_service("ConversionActionService") |
| 153 | + |
| 154 | + click_conversion = client.get_type("ClickConversion") |
| 155 | + click_conversion.conversion_action = conversion_action_service.conversion_action_path( |
| 156 | + args.customer_id, args.conversion_action_id |
| 157 | + ) |
| 158 | + click_conversion.gclid = args.gclid |
| 159 | + click_conversion.conversion_date_time = args.conversion_time |
| 160 | + click_conversion.conversion_value = args.value |
| 161 | + click_conversion.currency_code = args.currency |
| 162 | + |
| 163 | + response = conversion_upload_service.upload_click_conversions( |
| 164 | + customer_id=args.customer_id, |
| 165 | + conversions=[click_conversion], |
| 166 | + partial_failure=True, |
| 167 | + ) |
| 168 | + |
| 169 | + if response.partial_failure_error: |
| 170 | + print(f"Partial failure: {response.partial_failure_error.message}", file=sys.stderr) |
| 171 | + else: |
| 172 | + for result in response.results: |
| 173 | + print(f"Uploaded conversion: gclid={result.gclid}, action={result.conversion_action}") |
| 174 | + |
| 175 | + |
| 176 | +def main(): |
| 177 | + parser = argparse.ArgumentParser( |
| 178 | + description="Google Ads CLI for managing conversions", |
| 179 | + ) |
| 180 | + subparsers = parser.add_subparsers(dest="command", required=True) |
| 181 | + |
| 182 | + # list-conversions |
| 183 | + p_list = subparsers.add_parser("list-conversions", help="List all conversion actions") |
| 184 | + p_list.add_argument("--customer-id", required=True, help="Google Ads customer ID (no dashes)") |
| 185 | + p_list.set_defaults(func=list_conversions) |
| 186 | + |
| 187 | + # create-conversion |
| 188 | + p_create = subparsers.add_parser("create-conversion", help="Create a conversion action") |
| 189 | + p_create.add_argument("--customer-id", required=True, help="Google Ads customer ID (no dashes)") |
| 190 | + p_create.add_argument("--name", required=True, help="Conversion action name") |
| 191 | + p_create.add_argument("--category", default="PAGE_VIEW", help="Category (e.g. PAGE_VIEW, PURCHASE)") |
| 192 | + p_create.add_argument("--type", default="WEBPAGE", help="Type (e.g. WEBPAGE, UPLOAD_CLICKS)") |
| 193 | + p_create.add_argument("--value", type=float, default=None, help="Default conversion value") |
| 194 | + p_create.set_defaults(func=create_conversion) |
| 195 | + |
| 196 | + # get-tag-snippets |
| 197 | + p_tag = subparsers.add_parser("get-tag-snippets", help="Get tag snippets for a conversion action") |
| 198 | + p_tag.add_argument("--customer-id", required=True, help="Google Ads customer ID (no dashes)") |
| 199 | + p_tag.add_argument("--conversion-action-id", required=True, help="Conversion action ID") |
| 200 | + p_tag.set_defaults(func=get_tag_snippets) |
| 201 | + |
| 202 | + # check-status |
| 203 | + p_status = subparsers.add_parser("check-status", help="Check conversion tracking status") |
| 204 | + p_status.add_argument("--customer-id", required=True, help="Google Ads customer ID (no dashes)") |
| 205 | + p_status.set_defaults(func=check_status) |
| 206 | + |
| 207 | + # upload-conversion |
| 208 | + p_upload = subparsers.add_parser("upload-conversion", help="Upload an offline click conversion") |
| 209 | + p_upload.add_argument("--customer-id", required=True, help="Google Ads customer ID (no dashes)") |
| 210 | + p_upload.add_argument("--conversion-action-id", required=True, help="Conversion action ID") |
| 211 | + p_upload.add_argument("--gclid", required=True, help="Google click ID") |
| 212 | + p_upload.add_argument("--conversion-time", required=True, help="Conversion time (e.g. 2026-02-18 12:00:00-05:00)") |
| 213 | + p_upload.add_argument("--value", type=float, default=1.0, help="Conversion value (default: 1.0)") |
| 214 | + p_upload.add_argument("--currency", default="USD", help="Currency code (default: USD)") |
| 215 | + p_upload.set_defaults(func=upload_conversion) |
| 216 | + |
| 217 | + args = parser.parse_args() |
| 218 | + |
| 219 | + try: |
| 220 | + args.func(args) |
| 221 | + except GoogleAdsException as ex: |
| 222 | + print(f"Google Ads API error: {ex.error.code().name}", file=sys.stderr) |
| 223 | + for error in ex.failure.errors: |
| 224 | + print(f" {error.message}", file=sys.stderr) |
| 225 | + sys.exit(1) |
| 226 | + |
| 227 | + |
| 228 | +if __name__ == "__main__": |
| 229 | + main() |
0 commit comments