Skip to content

Commit 25fb458

Browse files
authored
I hate gads (#181)
1 parent cc7a0d0 commit 25fb458

File tree

9 files changed

+353
-0
lines changed

9 files changed

+353
-0
lines changed

web/io/app/layout.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Metadata } from "next";
2+
import Script from "next/script";
23
import { Geist, Geist_Mono } from "next/font/google";
34
import "./globals.css";
45
import Footer from "@/components/Footer";
@@ -47,6 +48,20 @@ export default function RootLayout({
4748
}) {
4849
return (
4950
<html lang="en">
51+
<head>
52+
<Script
53+
src="https://www.googletagmanager.com/gtag/js?id=AW-17513523888"
54+
strategy="afterInteractive"
55+
/>
56+
<Script id="gtag-init" strategy="afterInteractive">
57+
{`
58+
window.dataLayer = window.dataLayer || [];
59+
function gtag(){dataLayer.push(arguments);}
60+
gtag('js', new Date());
61+
gtag('config', 'AW-17513523888');
62+
`}
63+
</Script>
64+
</head>
5065
<body
5166
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
5267
>

web/pages/templates/corpan.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,14 @@
192192
}
193193
}
194194
</style>
195+
<!-- Google tag (gtag.js) -->
196+
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-17513523888"></script>
197+
<script>
198+
window.dataLayer = window.dataLayer || [];
199+
function gtag(){dataLayer.push(arguments);}
200+
gtag('js', new Date());
201+
gtag('config', 'AW-17513523888');
202+
</script>
195203
</head>
196204
<body>
197205
<div class="container">

web/pages/templates/game-landing.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,14 @@
332332
}
333333
}
334334
</style>
335+
<!-- Google tag (gtag.js) -->
336+
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-17513523888"></script>
337+
<script>
338+
window.dataLayer = window.dataLayer || [];
339+
function gtag(){dataLayer.push(arguments);}
340+
gtag('js', new Date());
341+
gtag('config', 'AW-17513523888');
342+
</script>
335343
</head>
336344
<body>
337345
<div class="container">

web/pages/templates/packs.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,14 @@
245245
}
246246
}
247247
</style>
248+
<!-- Google tag (gtag.js) -->
249+
<script async src="https://www.googletagmanager.com/gtag/js?id=AW-17513523888"></script>
250+
<script>
251+
window.dataLayer = window.dataLayer || [];
252+
function gtag(){dataLayer.push(arguments);}
253+
gtag('js', new Date());
254+
gtag('config', 'AW-17513523888');
255+
</script>
248256
</head>
249257
<body>
250258
<div class="container">

web/scripts/gads/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
google-ads.yaml
2+
*.json

web/scripts/gads/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Google Ads CLI
2+
3+
CLI tool for managing Google Ads conversions without the web UI.
4+
5+
## One-Time Setup
6+
7+
### 1. Developer Token
8+
9+
Go to [ads.google.com/aw/apicenter](https://ads.google.com/aw/apicenter) and apply for a developer token. Test access is approved instantly.
10+
11+
### 2. OAuth2 Credentials
12+
13+
1. Go to [Google Cloud Console](https://console.cloud.google.com/apis/credentials)
14+
2. Create a project (or use existing)
15+
3. Enable the **Google Ads API**
16+
4. Create **OAuth 2.0 Client ID** (Desktop app type)
17+
5. Note the `client_id` and `client_secret`
18+
19+
### 3. Refresh Token
20+
21+
```bash
22+
pip install google-ads
23+
python -c "from google.ads.googleads.client import GoogleAdsClient; GoogleAdsClient.generate_refresh_token()"
24+
```
25+
26+
Follow the prompts — you'll get a `refresh_token`.
27+
28+
### 4. Config File
29+
30+
```bash
31+
cp google-ads.yaml.example google-ads.yaml
32+
```
33+
34+
Fill in all fields. This file is gitignored.
35+
36+
## Install
37+
38+
```bash
39+
pip install -r requirements.txt
40+
```
41+
42+
## Usage
43+
44+
```bash
45+
# List conversion actions
46+
python cli.py list-conversions --customer-id 1234567890
47+
48+
# Create a page-view conversion
49+
python cli.py create-conversion \
50+
--customer-id 1234567890 \
51+
--name "Encorpora Page View" \
52+
--category PAGE_VIEW \
53+
--type WEBPAGE
54+
55+
# Get tag snippets
56+
python cli.py get-tag-snippets \
57+
--customer-id 1234567890 \
58+
--conversion-action-id 987654321
59+
60+
# Check tracking status
61+
python cli.py check-status --customer-id 1234567890
62+
63+
# Upload offline conversion
64+
python cli.py upload-conversion \
65+
--customer-id 1234567890 \
66+
--conversion-action-id 987654321 \
67+
--gclid "test-click-id" \
68+
--conversion-time "2026-02-18 12:00:00-05:00" \
69+
--value 1.0
70+
```
71+
72+
## What Requires the UI
73+
74+
- Initial developer token application (one-time)
75+
- OAuth2 consent screen setup (one-time)
76+
- Tag Assistant verification (browser-based)
77+
- Accepting ToS (one-time)

web/scripts/gads/cli.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
developer_token: "INSERT_DEVELOPER_TOKEN"
2+
client_id: "INSERT_CLIENT_ID"
3+
client_secret: "INSERT_CLIENT_SECRET"
4+
refresh_token: "INSERT_REFRESH_TOKEN"
5+
login_customer_id: "INSERT_LOGIN_CUSTOMER_ID"

web/scripts/gads/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
google-ads

0 commit comments

Comments
 (0)