Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "google-apps-ads-script",
|
||||
"description": "Comprehensive automation toolkit with 2 specialized skills: Google Apps Script for Workspace automation (Sheets, Docs, Gmail, Drive, Calendar) and Google Ads Scripts for campaign automation, bidding strategies, and reporting. Perfect for marketers and automation engineers.",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Henrik Soederlund",
|
||||
"email": "whom-wealthy.2z@icloud.com"
|
||||
},
|
||||
"skills": [
|
||||
"./skills"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# google-apps-ads-script
|
||||
|
||||
Comprehensive automation toolkit with 2 specialized skills: Google Apps Script for Workspace automation (Sheets, Docs, Gmail, Drive, Calendar) and Google Ads Scripts for campaign automation, bidding strategies, and reporting. Perfect for marketers and automation engineers.
|
||||
61
plugin.lock.json
Normal file
61
plugin.lock.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:henkisdabro/wookstar-claude-code-plugins:google-apps-ads-script",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "e189ade6df7f385263e08753fbbc125cb678fce0",
|
||||
"treeHash": "2ff9d94b8b0423f501ed58a2308236632803ab539e1c71da7d1a0d1315e88d5a",
|
||||
"generatedAt": "2025-11-28T10:17:25.728053Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "google-apps-ads-script",
|
||||
"description": "Comprehensive automation toolkit with 2 specialized skills: Google Apps Script for Workspace automation (Sheets, Docs, Gmail, Drive, Calendar) and Google Ads Scripts for campaign automation, bidding strategies, and reporting. Perfect for marketers and automation engineers.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "ee8bbe41c532b64ffc5ebbdb8be5185a94487cf7354e8b54d505a568fc9b0829"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "99e2427c474a49c4fca60a1373d2bb356959a15dae3d38af61158b1799dcdb14"
|
||||
},
|
||||
{
|
||||
"path": "skills/google-ads-scripts/SKILL.md",
|
||||
"sha256": "14fd5fa210bfb5e4f35d23c815f3728db03b1174e3f0f78263da15f5a01c95ac"
|
||||
},
|
||||
{
|
||||
"path": "skills/google-ads-scripts/references/ads-api-reference.md",
|
||||
"sha256": "6682095fefb4215365e6110ec8221aa4f3c48ee684737e57c71712d9d0cd70e1"
|
||||
},
|
||||
{
|
||||
"path": "skills/google-ads-scripts/scripts/validators.py",
|
||||
"sha256": "495f7cc3ebd7946060b532567b79d221788e3b8ac3348cde543c3f5003a6ba0b"
|
||||
},
|
||||
{
|
||||
"path": "skills/google-ads-scripts/assets/campaign-optimizer-template.js",
|
||||
"sha256": "4ab86b2c666c3a673daca4d582fb46bea953b8b7b0329a45e138e6e5e2131b48"
|
||||
},
|
||||
{
|
||||
"path": "skills/google-ads-scripts/assets/bid-manager-template.js",
|
||||
"sha256": "0cf960d1ece0441f09c6071a236003adaeb278c5bc9fc1fdab08d90d63020227"
|
||||
}
|
||||
],
|
||||
"dirSha256": "2ff9d94b8b0423f501ed58a2308236632803ab539e1c71da7d1a0d1315e88d5a"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
369
skills/google-ads-scripts/SKILL.md
Normal file
369
skills/google-ads-scripts/SKILL.md
Normal file
@@ -0,0 +1,369 @@
|
||||
---
|
||||
name: google-ads-scripts
|
||||
description: Expert guidance for Google Ads Script development including AdsApp API, campaign management, ad groups, keywords, bidding strategies, performance reporting, budget management, automated rules, and optimization patterns. Use when automating Google Ads campaigns, managing keywords and bids, creating performance reports, implementing automated rules, optimizing ad spend, working with campaign budgets, monitoring quality scores, tracking conversions, pausing low-performing keywords, adjusting bids based on ROAS, or building Google Ads automation scripts. Covers campaign operations, keyword targeting, bid optimization, conversion tracking, error handling, and JavaScript-based automation in Google Ads editor.
|
||||
---
|
||||
|
||||
# Google Ads Scripts
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides comprehensive guidance for developing Google Ads Scripts using the AdsApp API. Google Ads Scripts enable automation of campaign management, bid optimization, performance reporting, and bulk operations through JavaScript code that runs directly in the Google Ads editor.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Invoke this skill when:
|
||||
|
||||
- Automating Google Ads campaign management operations
|
||||
- Managing keywords and adjusting bids programmatically
|
||||
- Creating performance reports and dashboards
|
||||
- Implementing automated rules for campaign optimization
|
||||
- Optimizing ad spend based on ROAS or CPA targets
|
||||
- Working with campaign budgets and spend limits
|
||||
- Monitoring quality scores and pausing low-performers
|
||||
- Tracking conversions and adjusting strategies
|
||||
- Building bulk operations for large-scale account management
|
||||
- Implementing time-based or performance-based automation
|
||||
- Debugging Google Ads Script code or API issues
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
### 1. Campaign Operations
|
||||
|
||||
Manage campaigns programmatically including creation, modification, status changes, and bulk updates. The AdsApp.campaigns() selector provides filtering and iteration patterns.
|
||||
|
||||
**Common operations:**
|
||||
- Get campaigns with conditions (status, budget, name patterns)
|
||||
- Modify campaign properties (name, budget, dates, status)
|
||||
- Apply labels for organization
|
||||
- Pause/enable campaigns based on performance criteria
|
||||
|
||||
### 2. Keyword & Bid Management
|
||||
|
||||
Automate keyword targeting and bid adjustments based on performance metrics.
|
||||
|
||||
**Common operations:**
|
||||
- Get keywords with quality score filtering
|
||||
- Adjust max CPC bids based on ROAS/CPA targets
|
||||
- Add/remove negative keywords
|
||||
- Monitor keyword-level conversions
|
||||
- Implement bid optimization algorithms
|
||||
|
||||
### 3. Performance Reporting
|
||||
|
||||
Generate custom reports using campaign, ad group, keyword, and ad statistics.
|
||||
|
||||
**Common operations:**
|
||||
- Retrieve metrics for custom date ranges
|
||||
- Calculate derived metrics (CTR, CPC, conversion rate)
|
||||
- Export data to Google Sheets
|
||||
- Create automated dashboards
|
||||
- Monitor KPIs and trigger alerts
|
||||
|
||||
### 4. Budget Management
|
||||
|
||||
Control spending and allocate budgets across campaigns.
|
||||
|
||||
**Common operations:**
|
||||
- Get/set daily campaign budgets
|
||||
- Monitor spend against thresholds
|
||||
- Pause campaigns when budget limits reached
|
||||
- Distribute budgets based on performance
|
||||
|
||||
### 5. Automated Rules & Optimization
|
||||
|
||||
Build intelligence into campaign management with automated decision-making.
|
||||
|
||||
**Common operations:**
|
||||
- Pause low-performing keywords (low QS, high CPC, no conversions)
|
||||
- Increase bids for high-performers
|
||||
- Adjust budgets based on day-of-week patterns
|
||||
- Implement custom bidding strategies
|
||||
|
||||
### 6. Error Handling & Resilience
|
||||
|
||||
Implement robust error handling to manage API limits, quota issues, and runtime errors.
|
||||
|
||||
**Key patterns:**
|
||||
- Try-catch blocks for error handling
|
||||
- Null checks for optional properties
|
||||
- Logging to sheets for audit trails
|
||||
- Rate limiting awareness (30-minute execution limit)
|
||||
|
||||
## Quick Start Examples
|
||||
|
||||
### Example 1: Pause Low-Quality Keywords
|
||||
|
||||
```javascript
|
||||
function pauseLowQualityKeywords() {
|
||||
const keywords = AdsApp.keywords()
|
||||
.withCondition('keyword.status = ENABLED')
|
||||
.withCondition('keyword.quality_info.quality_score < 4')
|
||||
.withCondition('keyword.metrics.cost > 100000000') // >100 spend
|
||||
.get();
|
||||
|
||||
let count = 0;
|
||||
while (keywords.hasNext()) {
|
||||
const keyword = keywords.next();
|
||||
keyword.pause();
|
||||
count++;
|
||||
}
|
||||
|
||||
Logger.log(`Paused ${count} low-quality keywords`);
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Optimize Bids Based on ROAS
|
||||
|
||||
```javascript
|
||||
function optimizeBidsByROAS() {
|
||||
const TARGET_ROAS = 3.0; // 300%
|
||||
|
||||
const keywords = AdsApp.keywords()
|
||||
.withCondition('keyword.status = ENABLED')
|
||||
.withCondition('keyword.metrics.conversions > 5') // Min conversions
|
||||
.get();
|
||||
|
||||
while (keywords.hasNext()) {
|
||||
const keyword = keywords.next();
|
||||
const stats = keyword.getStatsFor('LAST_30_DAYS');
|
||||
const roas = stats.getReturnOnAdSpend();
|
||||
const currentBid = keyword.getMaxCpc();
|
||||
|
||||
if (roas > TARGET_ROAS) {
|
||||
// Increase bid by 10%
|
||||
keyword.setMaxCpc(Math.floor(currentBid * 1.1));
|
||||
} else if (roas < TARGET_ROAS * 0.7) {
|
||||
// Decrease bid by 5%
|
||||
keyword.setMaxCpc(Math.floor(currentBid * 0.95));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Export Campaign Performance to Sheets
|
||||
|
||||
```javascript
|
||||
function exportCampaignPerformance() {
|
||||
const campaigns = AdsApp.campaigns()
|
||||
.withCondition('campaign.status = ENABLED')
|
||||
.orderBy('campaign.metrics.cost DESC')
|
||||
.get();
|
||||
|
||||
const report = [['Campaign', 'Clicks', 'Cost', 'Conversions', 'CPC', 'ROAS']];
|
||||
|
||||
while (campaigns.hasNext()) {
|
||||
const campaign = campaigns.next();
|
||||
const stats = campaign.getStatsFor('LAST_30_DAYS');
|
||||
|
||||
report.push([
|
||||
campaign.getName(),
|
||||
stats.getClicks(),
|
||||
stats.getCost() / 1000000, // Convert from micros
|
||||
stats.getConversions(),
|
||||
stats.getAverageCpc() / 1000000,
|
||||
stats.getReturnOnAdSpend()
|
||||
]);
|
||||
}
|
||||
|
||||
// Write to Google Sheets
|
||||
const ss = SpreadsheetApp.openById('YOUR_SHEET_ID');
|
||||
const sheet = ss.getSheetByName('Campaign Report') || ss.insertSheet('Campaign Report');
|
||||
sheet.clear();
|
||||
sheet.getRange(1, 1, report.length, report[0].length).setValues(report);
|
||||
}
|
||||
```
|
||||
|
||||
## Working with References
|
||||
|
||||
For comprehensive API documentation, code patterns, and detailed examples, see:
|
||||
|
||||
- **references/ads-api-reference.md** - Complete AdsApp API reference including all selectors, methods, conditions, statistics, and enterprise patterns
|
||||
|
||||
The reference file contains:
|
||||
- Complete API hierarchy and object model
|
||||
- All selector patterns and conditions
|
||||
- Statistics methods and date ranges
|
||||
- Campaign, ad group, keyword, and ad operations
|
||||
- Bidding strategy implementation details
|
||||
- Performance reporting patterns
|
||||
- Budget management techniques
|
||||
- Advanced targeting options
|
||||
- Error handling patterns
|
||||
- Performance optimization strategies
|
||||
- Quotas and limits reference
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Batch Operations
|
||||
|
||||
Avoid individual API calls in loops. Instead, collect entities and perform batch operations:
|
||||
|
||||
```javascript
|
||||
// ✅ Good - Batch collection
|
||||
const keywords = AdsApp.keywords()
|
||||
.withCondition('keyword.quality_info.quality_score < 5')
|
||||
.get();
|
||||
|
||||
const toUpdate = [];
|
||||
while (keywords.hasNext()) {
|
||||
toUpdate.push(keywords.next());
|
||||
}
|
||||
|
||||
toUpdate.forEach(keyword => keyword.setMaxCpc(50000));
|
||||
```
|
||||
|
||||
### 2. Filter with Conditions
|
||||
|
||||
Use `.withCondition()` to filter at the API level rather than in JavaScript:
|
||||
|
||||
```javascript
|
||||
// ✅ Good - API-level filtering
|
||||
const campaigns = AdsApp.campaigns()
|
||||
.withCondition('campaign.status = ENABLED')
|
||||
.withCondition('campaign.budget_information.budget_amount > 100000000')
|
||||
.get();
|
||||
```
|
||||
|
||||
### 3. Handle Errors Gracefully
|
||||
|
||||
Always wrap operations in try-catch blocks and log errors:
|
||||
|
||||
```javascript
|
||||
function safeOperation() {
|
||||
try {
|
||||
// Operation code
|
||||
} catch (error) {
|
||||
Logger.log('Error: ' + error.message);
|
||||
Logger.log('Stack: ' + error.stack);
|
||||
|
||||
// Optionally email alert
|
||||
MailApp.sendEmail(
|
||||
Session.getEffectiveUser().getEmail(),
|
||||
'Ads Script Error',
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Respect Execution Limits
|
||||
|
||||
Scripts have a 30-minute execution timeout. For large accounts:
|
||||
- Limit result sets with `.withLimit()`
|
||||
- Process in batches
|
||||
- Use early termination when needed
|
||||
|
||||
### 5. Convert Micros Correctly
|
||||
|
||||
Google Ads API uses micros (1/1,000,000) for currency values:
|
||||
|
||||
```javascript
|
||||
const costMicros = stats.getCost();
|
||||
const costCurrency = costMicros / 1000000; // Convert to dollars/local currency
|
||||
|
||||
const bidCurrency = 5.00; // $5.00
|
||||
const bidMicros = bidCurrency * 1000000; // 5000000 micros
|
||||
```
|
||||
|
||||
### 6. Log Operations for Auditing
|
||||
|
||||
Maintain audit trails by logging changes to Google Sheets:
|
||||
|
||||
```javascript
|
||||
function logChange(operation, entity, details) {
|
||||
const ss = SpreadsheetApp.openById('LOG_SHEET_ID');
|
||||
const sheet = ss.getSheetByName('Audit Log');
|
||||
sheet.appendRow([
|
||||
new Date(),
|
||||
operation,
|
||||
entity,
|
||||
JSON.stringify(details)
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Integration with Other Skills
|
||||
|
||||
- **google-apps-script** - Use for Google Sheets reporting, Gmail notifications, Drive file management, and trigger setup
|
||||
- **ga4-measurement-protocol** - Combine with GA4 for tracking script-triggered events
|
||||
- **gtm-api** - Coordinate with GTM configurations for holistic tracking
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Conditional Bid Adjustment
|
||||
|
||||
```javascript
|
||||
function adjustBidsBasedOnDayOfWeek() {
|
||||
const today = new Date().getDay(); // 0 = Sunday, 6 = Saturday
|
||||
const isWeekend = today === 0 || today === 6;
|
||||
|
||||
const campaigns = AdsApp.campaigns()
|
||||
.withCondition('campaign.status = ENABLED')
|
||||
.get();
|
||||
|
||||
while (campaigns.hasNext()) {
|
||||
const campaign = campaigns.next();
|
||||
const budget = campaign.getBudget().getAmount();
|
||||
|
||||
if (isWeekend) {
|
||||
campaign.getBudget().setAmount(budget * 1.2); // +20% on weekends
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Quality Score Monitoring
|
||||
|
||||
```javascript
|
||||
function monitorQualityScores() {
|
||||
const threshold = 5;
|
||||
|
||||
const lowQualityKeywords = AdsApp.keywords()
|
||||
.withCondition(`keyword.quality_info.quality_score < ${threshold}`)
|
||||
.withCondition('keyword.status = ENABLED')
|
||||
.orderBy('keyword.metrics.cost DESC') // Most expensive first
|
||||
.withLimit(100)
|
||||
.get();
|
||||
|
||||
const alerts = [];
|
||||
while (lowQualityKeywords.hasNext()) {
|
||||
const keyword = lowQualityKeywords.next();
|
||||
alerts.push({
|
||||
keyword: keyword.getText(),
|
||||
qualityScore: keyword.getQualityScore(),
|
||||
cost: keyword.getStatsFor('LAST_7_DAYS').getCost() / 1000000
|
||||
});
|
||||
}
|
||||
|
||||
if (alerts.length > 0) {
|
||||
// Send email or log to sheet
|
||||
Logger.log(`${alerts.length} keywords with QS < ${threshold}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Validation & Testing
|
||||
|
||||
Use the validation scripts in `scripts/` for pre-deployment checks:
|
||||
|
||||
- **scripts/validators.py** - Validate campaign data, bid values, budget amounts before applying changes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Common Issues:**
|
||||
|
||||
1. **Execution timeout** - Reduce scope with `.withLimit()` or process in batches
|
||||
2. **Quota exceeded** - Reduce API call frequency, use cached data
|
||||
3. **Type errors** - Remember micros conversion for currency values
|
||||
4. **Null values** - Always check for null before accessing properties
|
||||
|
||||
**Debug with Logger:**
|
||||
|
||||
```javascript
|
||||
Logger.log('Debug info: ' + JSON.stringify(data));
|
||||
// View logs: View > Logs in script editor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
This skill provides production-ready patterns for Google Ads automation. Consult the comprehensive API reference for detailed method signatures and advanced use cases.
|
||||
391
skills/google-ads-scripts/assets/bid-manager-template.js
Normal file
391
skills/google-ads-scripts/assets/bid-manager-template.js
Normal file
@@ -0,0 +1,391 @@
|
||||
/**
|
||||
* Bid Manager Template
|
||||
*
|
||||
* Template for automated keyword bid management based on performance metrics.
|
||||
* Implements intelligent bidding strategies with safety controls.
|
||||
*
|
||||
* Features:
|
||||
* - Bid optimization based on ROAS/CPA targets
|
||||
* - Quality score-based bid adjustments
|
||||
* - Conversion-driven bid modifications
|
||||
* - Detailed audit logging
|
||||
* - Safety limits and dry-run mode
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
const CONFIG = {
|
||||
// Bidding strategy
|
||||
STRATEGY: 'ROAS', // 'ROAS' or 'CPA'
|
||||
TARGET_ROAS: 3.0, // Target Return on Ad Spend (300%)
|
||||
TARGET_CPA: 50.00, // Target Cost Per Acquisition (in currency)
|
||||
|
||||
// Bid adjustment factors
|
||||
HIGH_PERFORMER_INCREASE: 1.10, // +10% for high performers
|
||||
LOW_PERFORMER_DECREASE: 0.95, // -5% for low performers
|
||||
QUALITY_SCORE_FACTOR: 0.05, // ±5% per quality score point deviation
|
||||
|
||||
// Performance thresholds
|
||||
MIN_CONVERSIONS: 3, // Minimum conversions to evaluate
|
||||
MIN_CLICKS: 50, // Minimum clicks to evaluate
|
||||
HIGH_ROAS_THRESHOLD: 3.5, // ROAS above this = high performer
|
||||
LOW_ROAS_THRESHOLD: 2.0, // ROAS below this = low performer
|
||||
TARGET_QUALITY_SCORE: 7, // Target quality score
|
||||
|
||||
// Bid limits
|
||||
MIN_BID_MICROS: 50000, // $0.05 minimum bid
|
||||
MAX_BID_MICROS: 10000000, // $10.00 maximum bid
|
||||
MAX_BID_CHANGE_PERCENT: 25, // Maximum 25% bid change per run
|
||||
|
||||
// Date range
|
||||
DATE_RANGE: 'LAST_30_DAYS',
|
||||
|
||||
// Reporting
|
||||
SPREADSHEET_ID: 'YOUR_SPREADSHEET_ID', // Replace with your Sheet ID
|
||||
LOG_SHEET_NAME: 'Bid Manager Log',
|
||||
NOTIFICATION_EMAIL: Session.getEffectiveUser().getEmail(),
|
||||
|
||||
// Safety
|
||||
DRY_RUN: false, // Set to true to preview bid changes
|
||||
MAX_KEYWORDS_TO_PROCESS: 5000 // Prevent accidental mass changes
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// MAIN FUNCTION
|
||||
// ============================================================================
|
||||
|
||||
function main() {
|
||||
try {
|
||||
Logger.log('Starting Bid Manager...');
|
||||
Logger.log('Date: ' + new Date());
|
||||
Logger.log('Strategy: ' + CONFIG.STRATEGY);
|
||||
Logger.log('Mode: ' + (CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE'));
|
||||
|
||||
// Initialize reporting
|
||||
const sheet = initializeReportingSheet();
|
||||
|
||||
// Get keywords to optimize
|
||||
const keywords = getKeywordsToOptimize();
|
||||
Logger.log(`Found ${keywords.totalNumEntities()} keywords to evaluate`);
|
||||
|
||||
if (keywords.totalNumEntities() > CONFIG.MAX_KEYWORDS_TO_PROCESS) {
|
||||
throw new Error(`Too many keywords (${keywords.totalNumEntities()}). Increase MAX_KEYWORDS_TO_PROCESS if intended.`);
|
||||
}
|
||||
|
||||
// Process keywords
|
||||
const results = processKeywords(keywords);
|
||||
|
||||
// Generate summary
|
||||
logResults(sheet, results);
|
||||
sendEmailNotification(results);
|
||||
|
||||
Logger.log('Bid Manager completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// KEYWORD PROCESSING
|
||||
// ============================================================================
|
||||
|
||||
function getKeywordsToOptimize() {
|
||||
return AdsApp.keywords()
|
||||
.withCondition('keyword.status = ENABLED')
|
||||
.withCondition(`keyword.metrics.clicks >= ${CONFIG.MIN_CLICKS}`)
|
||||
.orderBy('keyword.metrics.cost DESC')
|
||||
.get();
|
||||
}
|
||||
|
||||
function processKeywords(keywords) {
|
||||
const results = {
|
||||
evaluated: 0,
|
||||
increased: [],
|
||||
decreased: [],
|
||||
paused: [],
|
||||
noChange: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
while (keywords.hasNext()) {
|
||||
const keyword = keywords.next();
|
||||
results.evaluated++;
|
||||
|
||||
try {
|
||||
const decision = evaluateKeyword(keyword);
|
||||
|
||||
if (decision.action === 'PAUSE') {
|
||||
if (!CONFIG.DRY_RUN) {
|
||||
keyword.pause();
|
||||
}
|
||||
results.paused.push({
|
||||
text: keyword.getText(),
|
||||
campaign: keyword.getCampaign().getName(),
|
||||
currentBid: keyword.getMaxCpc() / 1000000,
|
||||
reason: decision.reason
|
||||
});
|
||||
} else if (decision.newBid && decision.newBid !== keyword.getMaxCpc()) {
|
||||
const oldBid = keyword.getMaxCpc();
|
||||
|
||||
if (!CONFIG.DRY_RUN) {
|
||||
keyword.setMaxCpc(decision.newBid);
|
||||
}
|
||||
|
||||
const changeRecord = {
|
||||
text: keyword.getText(),
|
||||
campaign: keyword.getCampaign().getName(),
|
||||
oldBid: oldBid / 1000000,
|
||||
newBid: decision.newBid / 1000000,
|
||||
change: ((decision.newBid - oldBid) / oldBid * 100).toFixed(1) + '%',
|
||||
reason: decision.reason
|
||||
};
|
||||
|
||||
if (decision.newBid > oldBid) {
|
||||
results.increased.push(changeRecord);
|
||||
} else {
|
||||
results.decreased.push(changeRecord);
|
||||
}
|
||||
} else {
|
||||
results.noChange.push(keyword.getText());
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.errors.push({
|
||||
keyword: keyword.getText(),
|
||||
error: error.message
|
||||
});
|
||||
Logger.log(`Error processing keyword ${keyword.getText()}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function evaluateKeyword(keyword) {
|
||||
const stats = keyword.getStatsFor(CONFIG.DATE_RANGE);
|
||||
const conversions = stats.getConversions();
|
||||
const clicks = stats.getClicks();
|
||||
const cost = stats.getCost();
|
||||
const conversionValue = stats.getConversionValue();
|
||||
const currentBid = keyword.getMaxCpc();
|
||||
const qualityScore = keyword.getQualityScore();
|
||||
|
||||
// Insufficient data
|
||||
if (conversions < CONFIG.MIN_CONVERSIONS) {
|
||||
return { action: 'NONE', reason: `Insufficient conversions (${conversions})` };
|
||||
}
|
||||
|
||||
// Calculate performance metrics
|
||||
let performanceMultiplier = 1.0;
|
||||
let reason = [];
|
||||
|
||||
if (CONFIG.STRATEGY === 'ROAS') {
|
||||
const roas = cost > 0 ? conversionValue / (cost / 1000000) : 0;
|
||||
|
||||
if (roas >= CONFIG.HIGH_ROAS_THRESHOLD) {
|
||||
performanceMultiplier *= CONFIG.HIGH_PERFORMER_INCREASE;
|
||||
reason.push(`High ROAS (${roas.toFixed(2)})`);
|
||||
} else if (roas < CONFIG.LOW_ROAS_THRESHOLD) {
|
||||
performanceMultiplier *= CONFIG.LOW_PERFORMER_DECREASE;
|
||||
reason.push(`Low ROAS (${roas.toFixed(2)})`);
|
||||
}
|
||||
} else if (CONFIG.STRATEGY === 'CPA') {
|
||||
const cpa = conversions > 0 ? (cost / 1000000) / conversions : Infinity;
|
||||
|
||||
if (cpa <= CONFIG.TARGET_CPA * 0.8) {
|
||||
performanceMultiplier *= CONFIG.HIGH_PERFORMER_INCREASE;
|
||||
reason.push(`Low CPA ($${cpa.toFixed(2)})`);
|
||||
} else if (cpa > CONFIG.TARGET_CPA * 1.2) {
|
||||
performanceMultiplier *= CONFIG.LOW_PERFORMER_DECREASE;
|
||||
reason.push(`High CPA ($${cpa.toFixed(2)})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Quality score adjustment
|
||||
if (qualityScore !== null) {
|
||||
const qsDeviation = qualityScore - CONFIG.TARGET_QUALITY_SCORE;
|
||||
const qsMultiplier = 1 + (qsDeviation * CONFIG.QUALITY_SCORE_FACTOR);
|
||||
performanceMultiplier *= qsMultiplier;
|
||||
reason.push(`QS ${qualityScore}`);
|
||||
|
||||
// Pause very low quality keywords
|
||||
if (qualityScore < 3) {
|
||||
return {
|
||||
action: 'PAUSE',
|
||||
reason: `Very low quality score (${qualityScore})`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate new bid
|
||||
const proposedBid = Math.floor(currentBid * performanceMultiplier);
|
||||
|
||||
// Apply safety limits
|
||||
const safeBid = applySafetyLimits(currentBid, proposedBid);
|
||||
|
||||
// No change needed
|
||||
if (safeBid === currentBid) {
|
||||
return { action: 'NONE', reason: 'No change needed' };
|
||||
}
|
||||
|
||||
return {
|
||||
action: 'UPDATE_BID',
|
||||
newBid: safeBid,
|
||||
reason: reason.join(', ')
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BID CALCULATION & SAFETY
|
||||
// ============================================================================
|
||||
|
||||
function applySafetyLimits(currentBid, proposedBid) {
|
||||
// Enforce minimum bid
|
||||
if (proposedBid < CONFIG.MIN_BID_MICROS) {
|
||||
Logger.log(`Proposed bid ${proposedBid} below minimum, using ${CONFIG.MIN_BID_MICROS}`);
|
||||
return CONFIG.MIN_BID_MICROS;
|
||||
}
|
||||
|
||||
// Enforce maximum bid
|
||||
if (proposedBid > CONFIG.MAX_BID_MICROS) {
|
||||
Logger.log(`Proposed bid ${proposedBid} above maximum, using ${CONFIG.MAX_BID_MICROS}`);
|
||||
return CONFIG.MAX_BID_MICROS;
|
||||
}
|
||||
|
||||
// Enforce maximum change percentage
|
||||
const changePercent = Math.abs((proposedBid - currentBid) / currentBid * 100);
|
||||
if (changePercent > CONFIG.MAX_BID_CHANGE_PERCENT) {
|
||||
const maxChange = currentBid * (CONFIG.MAX_BID_CHANGE_PERCENT / 100);
|
||||
const cappedBid = proposedBid > currentBid
|
||||
? Math.floor(currentBid + maxChange)
|
||||
: Math.floor(currentBid - maxChange);
|
||||
Logger.log(`Change ${changePercent.toFixed(1)}% exceeds max ${CONFIG.MAX_BID_CHANGE_PERCENT}%, capping to ${cappedBid}`);
|
||||
return cappedBid;
|
||||
}
|
||||
|
||||
return proposedBid;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REPORTING
|
||||
// ============================================================================
|
||||
|
||||
function initializeReportingSheet() {
|
||||
const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
|
||||
let sheet = ss.getSheetByName(CONFIG.LOG_SHEET_NAME);
|
||||
|
||||
if (!sheet) {
|
||||
sheet = ss.insertSheet(CONFIG.LOG_SHEET_NAME);
|
||||
sheet.appendRow(['Timestamp', 'Mode', 'Action', 'Keyword', 'Campaign', 'Old Bid', 'New Bid', 'Change', 'Reason']);
|
||||
sheet.getRange('A1:I1').setFontWeight('bold');
|
||||
}
|
||||
|
||||
return sheet;
|
||||
}
|
||||
|
||||
function logResults(sheet, results) {
|
||||
const timestamp = new Date();
|
||||
const mode = CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE';
|
||||
|
||||
// Log bid increases
|
||||
results.increased.forEach(item => {
|
||||
sheet.appendRow([
|
||||
timestamp, mode, 'BID_INCREASED', item.text, item.campaign,
|
||||
item.oldBid, item.newBid, item.change, item.reason
|
||||
]);
|
||||
});
|
||||
|
||||
// Log bid decreases
|
||||
results.decreased.forEach(item => {
|
||||
sheet.appendRow([
|
||||
timestamp, mode, 'BID_DECREASED', item.text, item.campaign,
|
||||
item.oldBid, item.newBid, item.change, item.reason
|
||||
]);
|
||||
});
|
||||
|
||||
// Log paused keywords
|
||||
results.paused.forEach(item => {
|
||||
sheet.appendRow([
|
||||
timestamp, mode, 'PAUSED', item.text, item.campaign,
|
||||
item.currentBid, 0, '-100%', item.reason
|
||||
]);
|
||||
});
|
||||
|
||||
// Log errors
|
||||
results.errors.forEach(item => {
|
||||
sheet.appendRow([
|
||||
timestamp, mode, 'ERROR', item.keyword, '', '', '', '', item.error
|
||||
]);
|
||||
});
|
||||
|
||||
Logger.log(`Logged ${results.increased.length + results.decreased.length + results.paused.length + results.errors.length} actions to sheet`);
|
||||
}
|
||||
|
||||
function sendEmailNotification(results) {
|
||||
if (results.increased.length === 0 && results.decreased.length === 0 && results.paused.length === 0) {
|
||||
Logger.log('No bid changes, skipping email notification');
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = `Bid Manager Report - ${CONFIG.STRATEGY} - ${CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE'} - ${new Date().toDateString()}`;
|
||||
|
||||
const body = `
|
||||
Bid Manager Results
|
||||
===================
|
||||
|
||||
Mode: ${CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE'}
|
||||
Strategy: ${CONFIG.STRATEGY}
|
||||
${CONFIG.STRATEGY === 'ROAS' ? `Target ROAS: ${CONFIG.TARGET_ROAS}` : `Target CPA: $${CONFIG.TARGET_CPA}`}
|
||||
Date: ${new Date()}
|
||||
Keywords Evaluated: ${results.evaluated}
|
||||
|
||||
Actions Taken:
|
||||
--------------
|
||||
Bids Increased: ${results.increased.length}
|
||||
Bids Decreased: ${results.decreased.length}
|
||||
Keywords Paused: ${results.paused.length}
|
||||
No Change: ${results.noChange.length}
|
||||
Errors: ${results.errors.length}
|
||||
|
||||
Top Bid Increases:
|
||||
${results.increased.slice(0, 10).map(item =>
|
||||
`- ${item.text} (${item.campaign}): $${item.oldBid.toFixed(2)} → $${item.newBid.toFixed(2)} (${item.change}) - ${item.reason}`
|
||||
).join('\n')}
|
||||
|
||||
Top Bid Decreases:
|
||||
${results.decreased.slice(0, 10).map(item =>
|
||||
`- ${item.text} (${item.campaign}): $${item.oldBid.toFixed(2)} → $${item.newBid.toFixed(2)} (${item.change}) - ${item.reason}`
|
||||
).join('\n')}
|
||||
|
||||
Paused Keywords:
|
||||
${results.paused.map(item => `- ${item.text} (${item.campaign}): ${item.reason}`).join('\n')}
|
||||
|
||||
${results.errors.length > 0 ? `\nErrors:\n${results.errors.map(item => `- ${item.keyword}: ${item.error}`).join('\n')}` : ''}
|
||||
|
||||
---
|
||||
Generated by Google Ads Script
|
||||
`;
|
||||
|
||||
MailApp.sendEmail(CONFIG.NOTIFICATION_EMAIL, subject, body);
|
||||
Logger.log('Email notification sent to ' + CONFIG.NOTIFICATION_EMAIL);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ERROR HANDLING
|
||||
// ============================================================================
|
||||
|
||||
function handleError(error) {
|
||||
Logger.log('FATAL ERROR: ' + error.message);
|
||||
Logger.log('Stack trace: ' + error.stack);
|
||||
|
||||
MailApp.sendEmail(
|
||||
CONFIG.NOTIFICATION_EMAIL,
|
||||
'Bid Manager Error',
|
||||
`An error occurred in the Bid Manager script:\n\n${error.message}\n\nStack trace:\n${error.stack}`
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
303
skills/google-ads-scripts/assets/campaign-optimizer-template.js
Normal file
303
skills/google-ads-scripts/assets/campaign-optimizer-template.js
Normal file
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Campaign Optimizer Template
|
||||
*
|
||||
* Template for optimizing Google Ads campaigns based on performance metrics.
|
||||
* Customize thresholds and logic to fit your business needs.
|
||||
*
|
||||
* Features:
|
||||
* - Pause underperforming campaigns
|
||||
* - Adjust budgets based on ROAS
|
||||
* - Email notifications for actions taken
|
||||
* - Detailed logging to Google Sheets
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
const CONFIG = {
|
||||
// Performance thresholds
|
||||
MIN_CONVERSIONS: 5, // Minimum conversions to evaluate
|
||||
TARGET_ROAS: 3.0, // Target Return on Ad Spend (300%)
|
||||
LOW_ROAS_THRESHOLD: 2.0, // ROAS below this triggers budget reduction
|
||||
MIN_QUALITY_SCORE: 5, // Minimum acceptable quality score
|
||||
|
||||
// Budget adjustments
|
||||
BUDGET_INCREASE_FACTOR: 1.10, // +10% for high performers
|
||||
BUDGET_DECREASE_FACTOR: 0.90, // -10% for low performers
|
||||
|
||||
// Date range for analysis
|
||||
DATE_RANGE: 'LAST_30_DAYS',
|
||||
|
||||
// Reporting
|
||||
SPREADSHEET_ID: 'YOUR_SPREADSHEET_ID', // Replace with your Sheet ID
|
||||
LOG_SHEET_NAME: 'Campaign Optimizer Log',
|
||||
NOTIFICATION_EMAIL: Session.getEffectiveUser().getEmail(),
|
||||
|
||||
// Safety limits
|
||||
DRY_RUN: false, // Set to true to preview changes without applying
|
||||
MAX_CAMPAIGNS_TO_PROCESS: 1000 // Prevent accidental large-scale changes
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// MAIN FUNCTION
|
||||
// ============================================================================
|
||||
|
||||
function main() {
|
||||
try {
|
||||
Logger.log('Starting Campaign Optimizer...');
|
||||
Logger.log('Date: ' + new Date());
|
||||
Logger.log('Mode: ' + (CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE'));
|
||||
|
||||
// Initialize reporting
|
||||
const sheet = initializeReportingSheet();
|
||||
|
||||
// Get campaigns to optimize
|
||||
const campaigns = getCampaignsToOptimize();
|
||||
Logger.log(`Found ${campaigns.totalNumEntities()} campaigns to evaluate`);
|
||||
|
||||
if (campaigns.totalNumEntities() > CONFIG.MAX_CAMPAIGNS_TO_PROCESS) {
|
||||
throw new Error(`Too many campaigns (${campaigns.totalNumEntities()}). Increase MAX_CAMPAIGNS_TO_PROCESS if intended.`);
|
||||
}
|
||||
|
||||
// Process campaigns
|
||||
const results = processCampaigns(campaigns);
|
||||
|
||||
// Generate summary
|
||||
logResults(sheet, results);
|
||||
sendEmailNotification(results);
|
||||
|
||||
Logger.log('Campaign Optimizer completed successfully');
|
||||
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CAMPAIGN PROCESSING
|
||||
// ============================================================================
|
||||
|
||||
function getCampaignsToOptimize() {
|
||||
return AdsApp.campaigns()
|
||||
.withCondition('campaign.status = ENABLED')
|
||||
.orderBy('campaign.metrics.cost DESC')
|
||||
.get();
|
||||
}
|
||||
|
||||
function processCampaigns(campaigns) {
|
||||
const results = {
|
||||
evaluated: 0,
|
||||
paused: [],
|
||||
budgetIncreased: [],
|
||||
budgetDecreased: [],
|
||||
noAction: [],
|
||||
errors: []
|
||||
};
|
||||
|
||||
while (campaigns.hasNext()) {
|
||||
const campaign = campaigns.next();
|
||||
results.evaluated++;
|
||||
|
||||
try {
|
||||
const decision = evaluateCampaign(campaign);
|
||||
|
||||
if (decision.action === 'PAUSE') {
|
||||
if (!CONFIG.DRY_RUN) {
|
||||
campaign.pause();
|
||||
}
|
||||
results.paused.push({
|
||||
name: campaign.getName(),
|
||||
reason: decision.reason
|
||||
});
|
||||
} else if (decision.action === 'INCREASE_BUDGET') {
|
||||
if (!CONFIG.DRY_RUN) {
|
||||
increaseBudget(campaign, CONFIG.BUDGET_INCREASE_FACTOR);
|
||||
}
|
||||
results.budgetIncreased.push({
|
||||
name: campaign.getName(),
|
||||
reason: decision.reason
|
||||
});
|
||||
} else if (decision.action === 'DECREASE_BUDGET') {
|
||||
if (!CONFIG.DRY_RUN) {
|
||||
decreaseBudget(campaign, CONFIG.BUDGET_DECREASE_FACTOR);
|
||||
}
|
||||
results.budgetDecreased.push({
|
||||
name: campaign.getName(),
|
||||
reason: decision.reason
|
||||
});
|
||||
} else {
|
||||
results.noAction.push(campaign.getName());
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
results.errors.push({
|
||||
campaign: campaign.getName(),
|
||||
error: error.message
|
||||
});
|
||||
Logger.log(`Error processing campaign ${campaign.getName()}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function evaluateCampaign(campaign) {
|
||||
const stats = campaign.getStatsFor(CONFIG.DATE_RANGE);
|
||||
const conversions = stats.getConversions();
|
||||
const roas = stats.getReturnOnAdSpend();
|
||||
const cost = stats.getCost() / 1000000;
|
||||
|
||||
// Insufficient data
|
||||
if (conversions < CONFIG.MIN_CONVERSIONS) {
|
||||
return { action: 'NONE', reason: `Insufficient conversions (${conversions})` };
|
||||
}
|
||||
|
||||
// High performer - increase budget
|
||||
if (roas >= CONFIG.TARGET_ROAS) {
|
||||
return {
|
||||
action: 'INCREASE_BUDGET',
|
||||
reason: `High ROAS (${roas.toFixed(2)}), Cost: $${cost.toFixed(2)}`
|
||||
};
|
||||
}
|
||||
|
||||
// Low performer - pause
|
||||
if (roas < CONFIG.LOW_ROAS_THRESHOLD) {
|
||||
return {
|
||||
action: 'PAUSE',
|
||||
reason: `Low ROAS (${roas.toFixed(2)}) below threshold (${CONFIG.LOW_ROAS_THRESHOLD})`
|
||||
};
|
||||
}
|
||||
|
||||
// Medium performer - decrease budget
|
||||
if (roas < CONFIG.TARGET_ROAS) {
|
||||
return {
|
||||
action: 'DECREASE_BUDGET',
|
||||
reason: `ROAS (${roas.toFixed(2)}) below target (${CONFIG.TARGET_ROAS})`
|
||||
};
|
||||
}
|
||||
|
||||
return { action: 'NONE', reason: 'Performance within acceptable range' };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BUDGET MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
function increaseBudget(campaign, factor) {
|
||||
const currentBudget = campaign.getBudget().getAmount();
|
||||
const newBudget = Math.floor(currentBudget * factor);
|
||||
campaign.getBudget().setAmount(newBudget);
|
||||
Logger.log(`Increased budget for ${campaign.getName()}: ${currentBudget / 1000000} -> ${newBudget / 1000000}`);
|
||||
}
|
||||
|
||||
function decreaseBudget(campaign, factor) {
|
||||
const currentBudget = campaign.getBudget().getAmount();
|
||||
const newBudget = Math.floor(currentBudget * factor);
|
||||
campaign.getBudget().setAmount(newBudget);
|
||||
Logger.log(`Decreased budget for ${campaign.getName()}: ${currentBudget / 1000000} -> ${newBudget / 1000000}`);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REPORTING
|
||||
// ============================================================================
|
||||
|
||||
function initializeReportingSheet() {
|
||||
const ss = SpreadsheetApp.openById(CONFIG.SPREADSHEET_ID);
|
||||
let sheet = ss.getSheetByName(CONFIG.LOG_SHEET_NAME);
|
||||
|
||||
if (!sheet) {
|
||||
sheet = ss.insertSheet(CONFIG.LOG_SHEET_NAME);
|
||||
sheet.appendRow(['Timestamp', 'Mode', 'Action', 'Campaign', 'Reason']);
|
||||
sheet.getRange('A1:E1').setFontWeight('bold');
|
||||
}
|
||||
|
||||
return sheet;
|
||||
}
|
||||
|
||||
function logResults(sheet, results) {
|
||||
const timestamp = new Date();
|
||||
const mode = CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE';
|
||||
|
||||
// Log paused campaigns
|
||||
results.paused.forEach(item => {
|
||||
sheet.appendRow([timestamp, mode, 'PAUSED', item.name, item.reason]);
|
||||
});
|
||||
|
||||
// Log budget increases
|
||||
results.budgetIncreased.forEach(item => {
|
||||
sheet.appendRow([timestamp, mode, 'BUDGET_INCREASED', item.name, item.reason]);
|
||||
});
|
||||
|
||||
// Log budget decreases
|
||||
results.budgetDecreased.forEach(item => {
|
||||
sheet.appendRow([timestamp, mode, 'BUDGET_DECREASED', item.name, item.reason]);
|
||||
});
|
||||
|
||||
// Log errors
|
||||
results.errors.forEach(item => {
|
||||
sheet.appendRow([timestamp, mode, 'ERROR', item.campaign, item.error]);
|
||||
});
|
||||
|
||||
Logger.log(`Logged ${results.paused.length + results.budgetIncreased.length + results.budgetDecreased.length + results.errors.length} actions to sheet`);
|
||||
}
|
||||
|
||||
function sendEmailNotification(results) {
|
||||
if (results.paused.length === 0 && results.budgetIncreased.length === 0 && results.budgetDecreased.length === 0) {
|
||||
Logger.log('No actions taken, skipping email notification');
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = `Campaign Optimizer Report - ${CONFIG.DRY_RUN ? 'DRY RUN' : 'LIVE'} - ${new Date().toDateString()}`;
|
||||
|
||||
const body = `
|
||||
Campaign Optimizer Results
|
||||
==========================
|
||||
|
||||
Mode: ${CONFIG.DRY_RUN ? 'DRY RUN (preview only)' : 'LIVE'}
|
||||
Date: ${new Date()}
|
||||
Campaigns Evaluated: ${results.evaluated}
|
||||
|
||||
Actions Taken:
|
||||
--------------
|
||||
Paused: ${results.paused.length}
|
||||
Budget Increased: ${results.budgetIncreased.length}
|
||||
Budget Decreased: ${results.budgetDecreased.length}
|
||||
No Action: ${results.noAction.length}
|
||||
Errors: ${results.errors.length}
|
||||
|
||||
Paused Campaigns:
|
||||
${results.paused.map(item => `- ${item.name}: ${item.reason}`).join('\n')}
|
||||
|
||||
Budget Increased:
|
||||
${results.budgetIncreased.map(item => `- ${item.name}: ${item.reason}`).join('\n')}
|
||||
|
||||
Budget Decreased:
|
||||
${results.budgetDecreased.map(item => `- ${item.name}: ${item.reason}`).join('\n')}
|
||||
|
||||
${results.errors.length > 0 ? `\nErrors:\n${results.errors.map(item => `- ${item.campaign}: ${item.error}`).join('\n')}` : ''}
|
||||
|
||||
---
|
||||
Generated by Google Ads Script
|
||||
`;
|
||||
|
||||
MailApp.sendEmail(CONFIG.NOTIFICATION_EMAIL, subject, body);
|
||||
Logger.log('Email notification sent to ' + CONFIG.NOTIFICATION_EMAIL);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ERROR HANDLING
|
||||
// ============================================================================
|
||||
|
||||
function handleError(error) {
|
||||
Logger.log('FATAL ERROR: ' + error.message);
|
||||
Logger.log('Stack trace: ' + error.stack);
|
||||
|
||||
MailApp.sendEmail(
|
||||
CONFIG.NOTIFICATION_EMAIL,
|
||||
'Campaign Optimizer Error',
|
||||
`An error occurred in the Campaign Optimizer script:\n\n${error.message}\n\nStack trace:\n${error.stack}`
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
1379
skills/google-ads-scripts/references/ads-api-reference.md
Normal file
1379
skills/google-ads-scripts/references/ads-api-reference.md
Normal file
File diff suppressed because it is too large
Load Diff
407
skills/google-ads-scripts/scripts/validators.py
Normal file
407
skills/google-ads-scripts/scripts/validators.py
Normal file
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Google Ads Script Validators
|
||||
|
||||
Validation utilities for Google Ads campaigns, bids, budgets, and keywords.
|
||||
Use these validators before applying changes to ensure data integrity.
|
||||
|
||||
Based on Google Ads API specifications and best practices.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
import re
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Result of a validation check"""
|
||||
is_valid: bool
|
||||
errors: List[str]
|
||||
warnings: List[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.warnings is None:
|
||||
self.warnings = []
|
||||
|
||||
|
||||
class GoogleAdsValidators:
|
||||
"""Comprehensive validation for Google Ads Script operations"""
|
||||
|
||||
# Constants
|
||||
MIN_BUDGET = 0.01 # Minimum $0.01
|
||||
MAX_BUDGET = 999999.99 # Practical maximum
|
||||
MIN_BID = 0.01 # Minimum $0.01 in currency
|
||||
MIN_BID_MICROS = 10000 # Minimum 0.01 in micros
|
||||
MICROS_MULTIPLIER = 1000000
|
||||
|
||||
@staticmethod
|
||||
def is_campaign_name_valid(name: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate campaign name.
|
||||
|
||||
Rules:
|
||||
- Must be string
|
||||
- Length 1-255 characters
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
if not name or not isinstance(name, str):
|
||||
return False, "Campaign name must be a non-empty string"
|
||||
|
||||
if len(name) < 1 or len(name) > 255:
|
||||
return False, f"Campaign name must be 1-255 characters (got {len(name)})"
|
||||
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def is_budget_valid(cls, amount: float) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate budget amount in local currency.
|
||||
|
||||
Args:
|
||||
amount: Budget in local currency (e.g., 50.00 for $50)
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
if not isinstance(amount, (int, float)):
|
||||
return False, "Budget must be a number"
|
||||
|
||||
if amount < cls.MIN_BUDGET:
|
||||
return False, f"Budget must be at least {cls.MIN_BUDGET}"
|
||||
|
||||
if amount > cls.MAX_BUDGET:
|
||||
return False, f"Budget exceeds maximum of {cls.MAX_BUDGET}"
|
||||
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def is_bid_valid(cls, bid: float) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate bid amount in local currency.
|
||||
|
||||
Args:
|
||||
bid: Bid in local currency (e.g., 1.50 for $1.50)
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
if not isinstance(bid, (int, float)):
|
||||
return False, "Bid must be a number"
|
||||
|
||||
if bid < cls.MIN_BID:
|
||||
return False, f"Bid must be at least {cls.MIN_BID}"
|
||||
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def is_quality_score_valid(score: Any) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate quality score.
|
||||
|
||||
Args:
|
||||
score: Quality score (1-10 or None)
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
if score is None:
|
||||
return True, None # Quality score can be null
|
||||
|
||||
if not isinstance(score, int):
|
||||
return False, "Quality score must be an integer"
|
||||
|
||||
if score < 1 or score > 10:
|
||||
return False, "Quality score must be 1-10"
|
||||
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def is_cpc_bid_micros_valid(cls, bid_micros: int, max_bid: float = 10000) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate CPC bid in micros.
|
||||
|
||||
Args:
|
||||
bid_micros: Bid in micros (e.g., 50000 for $0.05)
|
||||
max_bid: Maximum allowed bid in currency
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
if not isinstance(bid_micros, int):
|
||||
return False, "Bid (micros) must be an integer"
|
||||
|
||||
if bid_micros < cls.MIN_BID_MICROS:
|
||||
return False, f"Bid must be at least {cls.MIN_BID_MICROS} micros ($0.01)"
|
||||
|
||||
max_bid_micros = int(max_bid * cls.MICROS_MULTIPLIER)
|
||||
if bid_micros > max_bid_micros:
|
||||
return False, f"Bid exceeds maximum of {max_bid_micros} micros (${max_bid})"
|
||||
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def is_campaign_status_valid(status: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate campaign status.
|
||||
|
||||
Args:
|
||||
status: Campaign status
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
valid_statuses = ['ENABLED', 'PAUSED', 'REMOVED']
|
||||
|
||||
if status not in valid_statuses:
|
||||
return False, f"Status must be one of {valid_statuses}"
|
||||
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def is_campaign_type_valid(campaign_type: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate campaign type.
|
||||
|
||||
Args:
|
||||
campaign_type: Campaign type
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
valid_types = ['SEARCH', 'DISPLAY', 'SHOPPING', 'VIDEO', 'PERFORMANCE_MAX']
|
||||
|
||||
if campaign_type not in valid_types:
|
||||
return False, f"Campaign type must be one of {valid_types}"
|
||||
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def is_keyword_text_valid(text: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate keyword text.
|
||||
|
||||
Args:
|
||||
text: Keyword text
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
if not text or not isinstance(text, str):
|
||||
return False, "Keyword text must be a non-empty string"
|
||||
|
||||
if len(text) < 1 or len(text) > 80:
|
||||
return False, f"Keyword text must be 1-80 characters (got {len(text)})"
|
||||
|
||||
return True, None
|
||||
|
||||
@staticmethod
|
||||
def is_match_type_valid(match_type: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate keyword match type.
|
||||
|
||||
Args:
|
||||
match_type: Match type
|
||||
|
||||
Returns:
|
||||
(is_valid, error_message)
|
||||
"""
|
||||
valid_types = ['BROAD', 'PHRASE', 'EXACT']
|
||||
|
||||
if match_type not in valid_types:
|
||||
return False, f"Match type must be one of {valid_types}"
|
||||
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def validate_campaign_update(cls, updates: Dict[str, Any]) -> ValidationResult:
|
||||
"""
|
||||
Validate campaign update payload.
|
||||
|
||||
Args:
|
||||
updates: Dictionary with campaign update fields
|
||||
|
||||
Returns:
|
||||
ValidationResult with is_valid and errors
|
||||
"""
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Validate name
|
||||
if 'name' in updates:
|
||||
is_valid, error = cls.is_campaign_name_valid(updates['name'])
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Validate budget
|
||||
if 'budget' in updates:
|
||||
is_valid, error = cls.is_budget_valid(updates['budget'])
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Validate status
|
||||
if 'status' in updates:
|
||||
is_valid, error = cls.is_campaign_status_valid(updates['status'])
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Validate type
|
||||
if 'type' in updates:
|
||||
is_valid, error = cls.is_campaign_type_valid(updates['type'])
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Validate dates
|
||||
if 'start_date' in updates and 'end_date' in updates:
|
||||
try:
|
||||
from datetime import datetime
|
||||
start = datetime.fromisoformat(updates['start_date'])
|
||||
end = datetime.fromisoformat(updates['end_date'])
|
||||
if end <= start:
|
||||
errors.append("End date must be after start date")
|
||||
except ValueError:
|
||||
errors.append("Invalid date format (use YYYY-MM-DD)")
|
||||
|
||||
return ValidationResult(
|
||||
is_valid=len(errors) == 0,
|
||||
errors=errors,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validate_keyword_update(cls, updates: Dict[str, Any]) -> ValidationResult:
|
||||
"""
|
||||
Validate keyword update payload.
|
||||
|
||||
Args:
|
||||
updates: Dictionary with keyword update fields
|
||||
|
||||
Returns:
|
||||
ValidationResult with is_valid and errors
|
||||
"""
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Validate keyword text
|
||||
if 'text' in updates:
|
||||
is_valid, error = cls.is_keyword_text_valid(updates['text'])
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Validate match type
|
||||
if 'match_type' in updates:
|
||||
is_valid, error = cls.is_match_type_valid(updates['match_type'])
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Validate bid (if in currency)
|
||||
if 'max_cpc' in updates:
|
||||
is_valid, error = cls.is_bid_valid(updates['max_cpc'])
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Validate bid (if in micros)
|
||||
if 'max_cpc_micros' in updates:
|
||||
is_valid, error = cls.is_cpc_bid_micros_valid(updates['max_cpc_micros'])
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
# Validate status
|
||||
if 'status' in updates:
|
||||
is_valid, error = cls.is_campaign_status_valid(updates['status'])
|
||||
if not is_valid:
|
||||
errors.append(error)
|
||||
|
||||
return ValidationResult(
|
||||
is_valid=len(errors) == 0,
|
||||
errors=errors,
|
||||
warnings=warnings
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def validate_quota_remaining(cls, current_cost: int, daily_limit: int, used: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Track API quota consumption.
|
||||
|
||||
Args:
|
||||
current_cost: Cost of current operation
|
||||
daily_limit: Daily quota limit
|
||||
used: Amount of quota already used
|
||||
|
||||
Returns:
|
||||
Dictionary with quota status
|
||||
"""
|
||||
remaining = daily_limit - used
|
||||
percent_used = (used / daily_limit) * 100 if daily_limit > 0 else 100
|
||||
|
||||
return {
|
||||
'remaining': remaining,
|
||||
'percent_used': percent_used,
|
||||
'is_critical': percent_used > 90,
|
||||
'should_throttle': percent_used > 85,
|
||||
'can_proceed': (used + current_cost) <= daily_limit
|
||||
}
|
||||
|
||||
|
||||
def currency_to_micros(amount: float) -> int:
|
||||
"""
|
||||
Convert currency to micros.
|
||||
|
||||
Args:
|
||||
amount: Amount in local currency (e.g., 5.00 for $5)
|
||||
|
||||
Returns:
|
||||
Amount in micros (e.g., 5000000)
|
||||
"""
|
||||
return int(amount * GoogleAdsValidators.MICROS_MULTIPLIER)
|
||||
|
||||
|
||||
def micros_to_currency(micros: int) -> float:
|
||||
"""
|
||||
Convert micros to currency.
|
||||
|
||||
Args:
|
||||
micros: Amount in micros (e.g., 5000000)
|
||||
|
||||
Returns:
|
||||
Amount in local currency (e.g., 5.00)
|
||||
"""
|
||||
return micros / GoogleAdsValidators.MICROS_MULTIPLIER
|
||||
|
||||
|
||||
# Example usage
|
||||
if __name__ == "__main__":
|
||||
validators = GoogleAdsValidators()
|
||||
|
||||
# Test campaign validation
|
||||
campaign_data = {
|
||||
'name': 'Q4 Sale Campaign',
|
||||
'budget': 5000,
|
||||
'status': 'ENABLED',
|
||||
'type': 'SEARCH'
|
||||
}
|
||||
|
||||
result = validators.validate_campaign_update(campaign_data)
|
||||
print(f"Campaign validation: {'✅ Valid' if result.is_valid else '❌ Invalid'}")
|
||||
if result.errors:
|
||||
print(f"Errors: {result.errors}")
|
||||
|
||||
# Test currency conversion
|
||||
bid_currency = 1.50
|
||||
bid_micros = currency_to_micros(bid_currency)
|
||||
print(f"\n${bid_currency} = {bid_micros} micros")
|
||||
print(f"{bid_micros} micros = ${micros_to_currency(bid_micros)}")
|
||||
|
||||
# Test keyword validation
|
||||
keyword_data = {
|
||||
'text': 'running shoes',
|
||||
'match_type': 'PHRASE',
|
||||
'max_cpc': 2.50
|
||||
}
|
||||
|
||||
result = validators.validate_keyword_update(keyword_data)
|
||||
print(f"\nKeyword validation: {'✅ Valid' if result.is_valid else '❌ Invalid'}")
|
||||
if result.errors:
|
||||
print(f"Errors: {result.errors}")
|
||||
Reference in New Issue
Block a user