-
-
Save siliconvallaeys/2dc8eb8b8f2fc992428ab2b66ce78f2f to your computer and use it in GitHub Desktop.
| /****************************************** | |
| * RSA Report | |
| * @version: 3.0 | |
| * @authors: Naman Jindal (Optmyzr), Frederick Vallaeys (Optmyzr) | |
| * ------------------------------- | |
| * Install this script in your Google Ads account (not an MCC account) | |
| * to generate a Google Sheet with a list of all your responsive search ads | |
| * and their headlines and descriptions. | |
| * For RSAs that are not using the maximum number of allowed variations, | |
| * this script will suggest new variations for headlines and descriptions | |
| * using the OpenAI GPT API. | |
| * The resulting sheet can be bulk uploaded back into Google Ads. | |
| * -------------------------------- | |
| * For more PPC tools, visit www.optmyzr.com. | |
| ******************************************/ | |
| // If Blank script will create a new Google sheet everytime it runs. | |
| var SS_URL = ''; | |
| // Name of the tab in the Google sheet. | |
| var TAB_NAME = ''; | |
| // Flag to decide if the script checks only ads in active campaigns and active ad groups | |
| var INCLUDE_PAUSED = true; | |
| // Script will only process this many or fewer ads to run within ChatGPT API limitations | |
| var MAX_ADS = 100; | |
| // Language code for output. Example US-EN or UK-EN, or FR-FR or CA-FR | |
| var LANGUAGE_CODE = 'US-EN'; | |
| // Use this to mention the type of industry your campaigns belongs to, to get more relevant results from ChatGPT API | |
| var INDUSTRY_TYPE = ''; | |
| // Use this to filter on specific campaigns by label. Case Sensitive. | |
| var CAMPAIGN_LABEL_IN = ['']; | |
| // Use this to filter on specific adgroups by label. Case Sensitive | |
| var ADGROUP_LABEL_IN = ['']; | |
| // Use this to filter on specific campaigns only. Case insensitive | |
| var CAMPAIGN_NAME_CONTAINS = ''; | |
| // only include ads with this many or fewer headlines on the output spreadsheet (defaults to 15) | |
| var MAX_HEADLINES = 15; | |
| // only include ads with this many or fewer descriptions on the output spreadsheet (defaults to 4) | |
| var MAX_DESCRIPTIONS = 4; | |
| // Multiple emails can be added sepearated by comma (,) | |
| // Used for access to spreadsheet and for sending email | |
| var EMAIL = ''; | |
| // Set to true if you want to recieve the report on Email. | |
| var SEND_EMAIL = false; | |
| var OPEN_AI_API_KEY = '' | |
| var GPT_MODEL = 'gpt-3.5-turbo'; | |
| // Do not edit anything below this line | |
| function main() { | |
| var output = [[ | |
| 'Account ID', 'Account Name', 'Campaign', 'Ad Group', 'Ad ID', '# Headlines', '# Descriptions', 'Ad Strength', | |
| 'Headline 1', 'Headline 2', 'Headline 3', 'Headline 4', 'Headline 5', 'Headline 6', 'Headline 7', 'Headline 8', 'Headline 9', | |
| 'Headline 10', 'Headline 11', 'Headline 12', 'Headline 13', 'Headline 14', 'Headline 15', | |
| 'Description Line 1', 'Description Line 2', 'Description Line 3', 'Description Line 4' | |
| ]]; | |
| var columCount = output[0].length; | |
| var backgroupHeader = []; | |
| while(backgroupHeader.length < columCount) { | |
| backgroupHeader.push('#ffffff'); | |
| } | |
| var backgrounds = [backgroupHeader]; | |
| var accId = AdsApp.currentAccount().getCustomerId(), | |
| customerId = AdsApp.currentAccount().getCustomerId().replace(/-/g, ''), | |
| accName = AdsApp.currentAccount().getName(); | |
| //var CAMPAIGN_IDS = []; | |
| var CAMPAIGN_LABELS_IN = []; | |
| if(CAMPAIGN_LABEL_IN.length) { | |
| var iter = AdsApp.labels().withCondition('label.name IN ("' + CAMPAIGN_LABEL_IN.join('","') + '")').get(); | |
| while(iter.hasNext()) { | |
| var label = iter.next(); | |
| CAMPAIGN_LABELS_IN.push('/customers/'+customerId+'/labels/'+label.getId()); | |
| } | |
| } | |
| var ADGROUP_LABELS_IN = []; | |
| if(ADGROUP_LABEL_IN.length) { | |
| var iter = AdsApp.labels().withCondition('label.name IN ("' + ADGROUP_LABEL_IN.join('","') + '")').get(); | |
| while(iter.hasNext()) { | |
| var label = iter.next(); | |
| ADGROUP_LABELS_IN.push('/customers/'+customerId+'/labels/'+label.getId()); | |
| } | |
| } | |
| var query = [ | |
| 'SELECT campaign.name, ad_group.name, ad_group_ad.ad.id, ad_group_ad.ad_strength,', | |
| 'ad_group_ad.ad.responsive_search_ad.headlines, ad_group_ad.ad.responsive_search_ad.descriptions', | |
| 'FROM ad_group_ad WHERE ad_group_ad.ad.type = RESPONSIVE_SEARCH_AD AND metrics.impressions >= 0', | |
| CAMPAIGN_NAME_CONTAINS ? 'AND campaign.name REGEXP_MATCH "(?i).*'+CAMPAIGN_NAME_CONTAINS+'.*"' : '', | |
| INCLUDE_PAUSED ? '' : 'AND ad_group_ad.status = ENABLED AND campaign.status = ENABLED and ad_group.status = ENABLED', | |
| CAMPAIGN_LABELS_IN.length ? 'AND campaign.labels CONTAINS ANY ("'+CAMPAIGN_LABELS_IN.join('","')+'")' : '', | |
| ADGROUP_LABELS_IN.length ? 'AND ad_group.labels CONTAINS ANY ("'+ADGROUP_LABELS_IN.join('","')+'")' : '', | |
| 'AND segments.date DURING LAST_7_DAYS' | |
| ].join(' '); | |
| var rows = AdsApp.report(query).rows(); | |
| while(rows.hasNext()) { | |
| var row = rows.next(); | |
| var headlines = row['ad_group_ad.ad.responsive_search_ad.headlines']; | |
| var headlineCount = headlines.length; | |
| var descriptions = row['ad_group_ad.ad.responsive_search_ad.descriptions']; | |
| var descriptionCount = descriptions.length; | |
| if(headlineCount > MAX_HEADLINES || descriptionCount > MAX_DESCRIPTIONS) { continue; } | |
| var out = [ | |
| accId, accName, row['campaign.name'], row['ad_group.name'], row['ad_group_ad.ad.id'], | |
| headlineCount, descriptionCount, row['ad_group_ad.ad_strength'] | |
| ]; | |
| var bgRow = ['#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff', '#ffffff']; | |
| var headlinesText = []; | |
| for(var z in headlines) { | |
| headlinesText.push(headlines[z].text); | |
| bgRow.push('#ffffff'); | |
| } | |
| var diff = 15 - headlinesText.length; | |
| var autoHeadlines = []; | |
| if(diff > 0) { | |
| var prompt = 'Find '+diff+' more ad headlines under 30 characters that are similar to these:\n'; | |
| if(LANGUAGE_CODE && INDUSTRY_TYPE){ | |
| prompt = 'Find '+diff+' more ad headlines, in ' + LANGUAGE_CODE +' language code, for a '+INDUSTRY_TYPE+' industry campaign under 30 characters that are similar to these:\n'; | |
| } else if(LANGUAGE_CODE){ | |
| prompt = 'Find '+diff+' more ad headlines, in ' + LANGUAGE_CODE +' language code, under 30 characters that are similar to these:\n'; | |
| } else if(INDUSTRY_TYPE){ | |
| prompt = 'Find '+diff+' more ad headlines for a '+INDUSTRY_TYPE+' industry campaign under 30 characters that are similar to these:\n'; | |
| } | |
| autoHeadlines = generateTextOpenAI(prompt, headlinesText); | |
| } | |
| if(autoHeadlines.length) { | |
| headlinesText = headlinesText.concat(autoHeadlines); | |
| for(var i = 0; i < autoHeadlines.length; i++) { | |
| bgRow.push('#d9ead3'); | |
| } | |
| } | |
| while(headlinesText.length < 15) { | |
| headlinesText.push(''); | |
| bgRow.push('#fffff') | |
| } | |
| while(headlinesText.length > 15) { | |
| headlinesText.pop(); | |
| bgRow.pop(); | |
| } | |
| var descriptionsText = []; | |
| for(var z in descriptions) { | |
| descriptionsText.push(descriptions[z].text); | |
| bgRow.push('#ffffff'); | |
| } | |
| var diff = 4 - descriptions.length; | |
| var autoDescriptions = []; | |
| if(diff > 0) { | |
| var prompt = 'Find '+diff+' more ad descriptions under 90 characters that are similar to these:\n'; | |
| if(LANGUAGE_CODE && INDUSTRY_TYPE){ | |
| prompt = 'Find '+diff+' more ad descriptions, in ' + LANGUAGE_CODE +' language code, for a '+INDUSTRY_TYPE+' industry campaign under 90 characters that are similar to these:\n'; | |
| } else if(LANGUAGE_CODE){ | |
| prompt = 'Find '+diff+' more ad descriptions, in ' + LANGUAGE_CODE +' language code, under 90 characters that are similar to these:\n'; | |
| } else if(INDUSTRY_TYPE){ | |
| prompt = 'Find '+diff+' more ad descriptions for a '+INDUSTRY_TYPE+' industry campaign under 90 characters that are similar to these:\n'; | |
| } | |
| autoDescriptions = generateTextOpenAI(prompt, descriptionsText); | |
| } | |
| if(autoDescriptions.length) { | |
| descriptionsText = descriptionsText.concat(autoDescriptions); | |
| for(var i = 0; i < autoDescriptions.length; i++) { | |
| bgRow.push('#d9ead3'); | |
| } | |
| } | |
| while(descriptionsText.length < 4) { | |
| descriptionsText.push(''); | |
| bgRow.push('#ffffff'); | |
| } | |
| while(descriptionsText.length > 4) { | |
| descriptionsText.pop(); | |
| bgRow.pop(); | |
| } | |
| out = out.concat(headlinesText).concat(descriptionsText); | |
| backgrounds.push(bgRow); | |
| output.push(out); | |
| if(output.length > MAX_ADS) { | |
| break; | |
| } | |
| } | |
| if(!SS_URL) { | |
| var ss = SpreadsheetApp.create(accName + ': RSA Report'); | |
| SS_URL = ss.getUrl(); | |
| if(EMAIL) { | |
| ss.addEditors(EMAIL.split(',')); | |
| } | |
| } | |
| Logger.log('Report URL: ' + SS_URL); | |
| var ss = SpreadsheetApp.openByUrl(SS_URL); | |
| var tab = ss.getSheetByName(TAB_NAME); | |
| if(!tab) { | |
| tab = ss.getSheetByName('Sheet1'); | |
| if(!tab) { | |
| tab = ss.insertSheet(TAB_NAME); | |
| } else { | |
| tab.setName(TAB_NAME) | |
| } | |
| } | |
| tab.clearContents(); | |
| tab.setFrozenRows(1); | |
| tab.getRange(1,1,output.length,output[0].length).setValues(output).setBackgrounds(backgrounds).setFontFamily('Calibri'); | |
| if(EMAIL && SEND_EMAIL) { | |
| MailApp.sendEmail(EMAIL, accName + ' RSA Report is ready', 'Report is available at below link:\n'+SS_URL); | |
| } | |
| } | |
| function getGoogleAdsFormattedDate(d, format){ | |
| var date = new Date(); | |
| date.setDate(date.getDate() - d); | |
| return Utilities.formatDate(date,AdsApp.currentAccount().getTimeZone(),format); | |
| } | |
| function generateTextOpenAI(question, texts) { | |
| //texts.pop(); | |
| var prompt = question + texts.join('\n'); | |
| var messages= [ | |
| {"role": "user", "content": prompt} | |
| ]; | |
| var payload = { | |
| "model": GPT_MODEL, | |
| "messages": messages | |
| }; | |
| var httpOptions = { | |
| "method" : "POST", | |
| "muteHttpExceptions": true, | |
| "contentType": "application/json", | |
| "headers" : { | |
| "Authorization" : 'Bearer ' + OPEN_AI_API_KEY | |
| }, | |
| 'payload': JSON.stringify(payload) | |
| }; | |
| //Logger.log(JSON.stringify(payload)); | |
| var response = JSON.parse(UrlFetchApp.fetch('https://api.openai.com/v1/chat/completions', httpOptions)); | |
| var choices = response['choices']; | |
| var texts = []; | |
| if(choices && choices[0] && choices[0]['message']) { | |
| var output = choices[0]['message']['content'].split('\n'); | |
| for(var z in output) { | |
| if(!output[z].trim()) { continue; } | |
| var parts = output[z].split('. '); | |
| if(parts.length > 1) { | |
| parts.shift(); | |
| } | |
| texts.push(parts.join('. ')); | |
| } | |
| } else { | |
| //Logger.log('No results found!') | |
| Logger.log(response); | |
| } | |
| return texts; | |
| } |
I have a GPT plus account however I get the same error: TypeError: Cannot read properties of undefined (reading '0')
at generateTextOpenAI (Code:221:13)
at main (Code:130:26)
at Object. (adsapp_compiled:19646:54)
I have a GPT plus account however I get the same error: TypeError: Cannot read properties of undefined (reading '0') at generateTextOpenAI (Code:221:13) at main (Code:130:26) at Object. (adsapp_compiled:19646:54)
Is a GPTplus account the same as having paid access to the API, for example this has cost me 16c so far?
I have a GPT plus account however I get the same error: TypeError: Cannot read properties of undefined (reading '0') at generateTextOpenAI (Code:221:13) at main (Code:130:26) at Object. (adsapp_compiled:19646:54)
Is a GPTplus account the same as having paid access to the API, for example this has cost me 16c so far?
Oh I see, I guess not. Where exactly did you purchase the access to the API and which one?
Thanks for the help btw, really appreciated
I have a GPT plus account however I get the same error: TypeError: Cannot read properties of undefined (reading '0') at generateTextOpenAI (Code:221:13) at main (Code:130:26) at Object. (adsapp_compiled:19646:54)
Is a GPTplus account the same as having paid access to the API, for example this has cost me 16c so far?
Oh I see, I guess not. Where exactly did you purchase the access to the API and which one? Thanks for the help btw, really appreciated
https://platform.openai.com/account/billing/overview
Then generate a new API key, make sure you set limits etc and obviously keep it SUPER secret otherwise you could get royally scruffed!
- I got a 30 min timeout, is there a way to extend it?
- Should I have the new script experience enabled?
- I would add a campaign id column because I get an error when I try to import the file.
- Add an option to do this for only enabled campaigns
Hi there, I'm very new to scripting and was wondering if there is a way to make the script above only run on a specific campaign name or campaign id? Any advice would be greatly appreciated! Thanks.
- this script will suggest new variations for headlines and descriptions *
This part doesn't work. It doesn't suggest new titles to me, it just shows me the existing ones
doesn't work
Its a free API issue, once you start paying it works, it will work on a single adgroup for free, but anymore and you have to have a paid account for OpenAI