Bids by Weather
Bids by Weather
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @name Bid By Weather
*
* @overview The Bid By Weather script adjusts campaign bids by weather
* conditions of their associated locations. See
* https://developers.google.com/google-ads/scripts/docs/solutions/weather-
based-campaign-management#bid-by-weather
* for more details.
*
* @author Google Ads Scripts Team [[email protected]]
*
* @version 2.0
*
* @changelog
* - version 2.0
* - Updated to use new Google Ads Scripts features.
* - version 1.2.2
* - Add support for video and shopping campaigns.
* - version 1.2.1
* - Added validation for external spreadsheet setup.
* - version 1.2
* - Added proximity based targeting. Targeting flag allows location
* targeting, proximity targeting or both.
* - version 1.1
* - Added flag allowing bid adjustments on all locations targeted by
* a campaign rather than only those that match the campaign rule
* - version 1.0
* - Released initial version.
*/
//in case you dont want to enable a adgroup after 2 days put below word in it name
// you can also change this word here !! Don't leave this empty
const ADGROUP_TO_NOT_ENABLE = "disabled"
// The email address you want the hourly update to be sent to.
// If you'd like to send to multiple addresses then have them separated by commas,
// for example ["[email protected]", "[email protected]"]
var EMAILS = ["[email protected]"];
/**
* According to the list of campaigns and their associated locations, the script
* makes a call to the OpenWeatherMap API for each location.
* Based on the weather conditions, the bids are adjusted.
*/
function main() {
var changesMade = [["Campaign","Adgroup","Weather","Change"]]
validateApiKey();
if(ADGROUP_TO_NOT_ENABLE.length < 1){
throw new Error('Please enter a ADGROUP_TO_NOT_ENABLE string.')
}
// check and revert changes if needed
var revertedChages = revertChanges()
// Load data from spreadsheet.
const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
const campaignRuleData = getSheetData(spreadsheet, 1);
const weatherConditionData = getSheetData(spreadsheet, 2);
const geoMappingData = getSheetData(spreadsheet, 3);
sendEmailMessage(changesMade, revertedChages)
}
/**
* Retrieves the data for a worksheet.
*
* @param {Object} spreadsheet The spreadsheet.
* @param {number} sheetIndex The sheet index.
* @return {Array} The data as a two dimensional array.
*/
function getSheetData(spreadsheet, sheetIndex) {
const sheet = spreadsheet.getSheets()[sheetIndex];
const range =
sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn());
return range.getValues();
}
/**
* Builds a mapping between the list of campaigns and the rules
* being applied to them.
*
* @param {Array} campaignRulesData The campaign rules data, from the
* spreadsheet.
* @return {!Object.<string, Array.<Object>> } A map, with key as campaign name,
* and value as an array of rules that apply to this campaign.
*/
function buildCampaignRulesMapping(campaignRulesData) {
const campaignMapping = {};
for (const rules of campaignRulesData) {
// Skip rule if not enabled.
if (rules[4].toLowerCase() == 'yes') {
const campaignName = rules[0];
const campaignRules = campaignMapping[campaignName] || [];
campaignRules.push({
'name': campaignName,
/**
* Builds a mapping between a weather condition name (e.g. Sunny) and the rules
* that correspond to that weather condition.
*
* @param {Array} weatherConditionData The weather condition data from the
* spreadsheet.
* @return {!Object.<string, Array.<Object>>} A map, with key as a weather
* condition name, and value as the set of rules corresponding to that
* weather condition.
*/
function buildWeatherConditionMapping(weatherConditionData) {
const weatherConditionMapping = {};
for (const weatherCondition of weatherConditionData) {
const weatherConditionName = weatherCondition[0];
weatherConditionMapping[weatherConditionName] = {
// Condition name (e.g. Sunny)
'condition': weatherConditionName,
/**
* Builds a mapping between a location name (as understood by OpenWeatherMap
* API) and a list of geo codes as identified by Google Ads scripts.
*
* @param {Array} geoTargetData The geo target data from the spreadsheet.
* @return {!Object.<string, Array.<Object>>} A map, with key as a locaton name,
* and value as an array of geo codes that correspond to that location
* name.
*/
function buildLocationMapping(geoTargetData) {
const locationMapping = {};
for (const geoTarget of geoTargetData) {
const locationName = geoTarget[0];
const locationDetails = locationMapping[locationName] || {
'geoCodes': [] // List of geo codes understood by Google Ads scripts.
};
locationDetails.geoCodes.push(geoTarget[1]);
locationMapping[locationName] = locationDetails;
}
Logger.log('Location Mapping: %s', locationMapping);
return locationMapping;
}
/**
* Applies rules to a campaign.
*
* @param {string} campaignName The name of the campaign.
* @param {Object} campaignRules The details of the campaign. See
* buildCampaignMapping for details.
* @param {Object} locationMapping Mapping between a location name (as
* understood by OpenWeatherMap API) and a list of geo codes as
* identified by Google Ads scripts. See buildLocationMapping for details.
* @param {Object} weatherConditionMapping Mapping between a weather condition
* name (e.g. Sunny) and the rules that correspond to that weather
* condition. See buildWeatherConditionMapping for details.
*/
function applyRulesForCampaign(campaignName, campaignRules, locationMapping,
weatherConditionMapping) {
var campaignData = []
for (const rules of campaignRules) {
let bidModifier = 1;
const campaignRule = rules;
const adgroup = campaignRule.adgroups
// Get the weather for the required location.
const locationDetails = locationMapping[campaignRule.location];
const weather = getWeather(campaignRule.location);
Logger.log('Weather for %s: %s', locationDetails, weather);
// Get the weather rules to be checked.
const weatherConditionName = campaignRule.condition;
const weatherConditionRules = weatherConditionMapping[weatherConditionName];
/**
* Converts a temperature value from kelvin to fahrenheit.
*
* @param {number} kelvin The temperature in Kelvin scale.
* @return {number} The temperature in Fahrenheit scale.
*/
function toFahrenheit(kelvin) {
return (kelvin - 273.15) * 1.8 + 32;
}
/**
* Evaluates the weather rules.
*
* @param {Object} weatherRules The weather rules to be evaluated.
* @param {Object.<string, string>} weather The actual weather.
* @return {boolean} True if the rule matches current weather conditions,
* False otherwise.
*/
function evaluateWeatherRules(weatherRules, weather) {
// See https://openweathermap.org/weather-data
// for values returned by OpenWeatherMap API.
let precipitation = 0;
if (weather.rain && weather.rain['3h']) {
precipitation = weather.rain['3h'];
}
const temperature = toFahrenheit(weather.main.temp);
const windspeed = weather.wind.speed;
/**
* Evaluates a condition for a value against a set of known evaluation rules.
*
* @param {string} condition The condition to be checked.
* @param {Object} value The value to be checked.
* @return {boolean} True if an evaluation rule matches, false otherwise.
*/
function evaluateMatchRules(condition, value) {
// No condition to evaluate, rule passes.
if (condition == '') {
return true;
}
const rules = [matchesBelow, matchesAbove, matchesRange];
/**
* Evaluates whether a value is below a threshold value.
*
* @param {string} condition The condition to be checked. (e.g. below 50).
* @param {number} value The value to be checked.
* @return {boolean} True if the value is less than what is specified in
* condition, false otherwise.
*/
function matchesBelow(condition, value) {
conditionParts = condition.split(' ');
if (conditionParts.length != 2) {
return false;
}
if (conditionParts[0] != 'below') {
return false;
}
/**
* Evaluates whether a value is above a threshold value.
*
* @param {string} condition The condition to be checked. (e.g. above 50).
* @param {number} value The value to be checked.
* @return {boolean} True if the value is greater than what is specified in
* condition, false otherwise.
*/
function matchesAbove(condition, value) {
conditionParts = condition.split(' ');
if (conditionParts.length != 2) {
return false;
}
if (conditionParts[0] != 'above') {
return false;
}
if (value > conditionParts[1]) {
return true;
}
return false;
}
/**
* Evaluates whether a value is within a range of values.
*
* @param {string} condition The condition to be checked (e.g. 5 to 18).
* @param {number} value The value to be checked.
* @return {boolean} True if the value is in the desired range, false otherwise.
*/
function matchesRange(condition, value) {
conditionParts = condition.replace('w+', ' ').split(' ');
if (conditionParts.length != 3) {
return false;
}
if (conditionParts[1] != 'to') {
return false;
}
/**
* Retrieves the weather for a given location, using the OpenWeatherMap API.
*
* @param {string} location The location to get the weather for.
* @return {Object.<string, string>} The weather attributes and values, as
* defined in the API.
*/
function getWeather(location) {
if (location in WEATHER_LOOKUP_CACHE) {
Logger.log('Cache hit...');
return WEATHER_LOOKUP_CACHE[location];
}
const url=`http://api.openweathermap.org/data/2.5/weather?APPID=$
{OPEN_WEATHER_MAP_API_KEY}&q=${location}`;
const response = UrlFetchApp.fetch(url);
if (response.getResponseCode() != 200) {
throw Utilities.formatString(
'Error returned by API: %s, Location searched: %s.',
response.getContentText(), location);
}
const result = JSON.parse(response.getContentText());
/**
* Adjusts the bidModifier for a list of geo codes for a campaign.
*
* @param {string} campaignName The name of the campaign.
* @param {Array} geoCodes The list of geo codes for which bids should be
* adjusted. If null, all geo codes on the campaign are adjusted.
* @param {number} bidModifier The bid modifier to use.
*/
function changeAdgroups(campaignName, adgroupString, weatherConditionName) {
var returnData = []
// Get the campaign.
const campaign = getCampaign(campaignName);
if (!campaign) return null;
var adgroupString = adgroupString.toLowerCase()
var campaignId = campaign.getId()
var adgroups = campaign.adGroups().get()
while (adgroups.hasNext()) {
var adGroup = adgroups.next();
var adgroupName = adGroup.getName()
var adgroupNameL = adgroupName.toLowerCase()
if(adgroupNameL.includes(adgroupString)){
adGroup.enable()
returnData.push([campaignName,adgroupName,weatherConditionName,"Enabled"])
}
else{
adGroup.pause()
returnData.push([campaignName,adgroupName,weatherConditionName,"Paused"])
}
}
const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
var storageSheet = spreadsheet.getSheetByName(STORAGE_SHEET_NAME);
var dates = getDates()
storageSheet.appendRow([campaignId, campaignName, adgroupString,
dates.today+"///", dates.afterTwoDays+"///" ])
return returnData
}
/**
* Finds a campaign by name, whether it is a regular, video, or shopping
* campaign, by trying all in sequence until it finds one.
*
* @param {string} campaignName The campaign name to find.
* @return {Object} The campaign found, or null if none was found.
*/
function getCampaign(campaignName) {
const selectors = [AdsApp.campaigns(), AdsApp.videoCampaigns(),
AdsApp.shoppingCampaigns()];
for (const selector of selectors) {
const campaignIter = selector.
withCondition(`CampaignName = "${campaignName}"`).
get();
if (campaignIter.hasNext()) {
return campaignIter.next();
}
}
return null;
}
/**
* DO NOT EDIT ANYTHING BELOW THIS LINE.
* Please modify your spreadsheet URL and API key at the top of the file only.
*/
/**
* Validates the provided spreadsheet URL to make sure that it's set up
* properly. Throws a descriptive error message if validation fails.
*
* @param {string} spreadsheeturl The URL of the spreadsheet to open.
* @return {Spreadsheet} The spreadsheet object itself, fetched from the URL.
* @throws {Error} If the spreadsheet URL hasn't been set
*/
function validateAndGetSpreadsheet(spreadsheeturl) {
if (spreadsheeturl == 'INSERT_SPREADSHEET_URL_HERE') {
throw new Error('Please specify a valid Spreadsheet URL. You can find' +
' a link to a template in the associated guide for this script.');
}
const spreadsheet = SpreadsheetApp.openByUrl(spreadsheeturl);
return spreadsheet;
}
/**
* Validates the provided API key to make sure that it's not the default. Throws
* a descriptive error message if validation fails.
*
* @throws {Error} If the configured API key hasn't been set.
*/
function validateApiKey() {
if (OPEN_WEATHER_MAP_API_KEY == 'INSERT_OPEN_WEATHER_MAP_API_KEY_HERE') {
throw new Error('Please specify a valid API key for OpenWeatherMap. You ' +
'can acquire one here: http://openweathermap.org/appid');
}
}
function revertChanges(){
var changes = [["Campaign","Adgroup","Change"]]
const spreadsheet = validateAndGetSpreadsheet(SPREADSHEET_URL);
var storageSheet = spreadsheet.getSheetByName(STORAGE_SHEET_NAME);
var dates = getDates ()
var today = dates.today
var data = storageSheet.getDataRange().getValues()
for (let i = 1; i < data.length; i++) {
const row = data[i];
var dateToCheck = row[4].replace("///", "")
if(dateToCheck == today){
var campaignName = row[1]
var adgroupString = row[2]
const campaign = getCampaign(campaignName);
var adgroups = campaign.adGroups().get()
while (adgroups.hasNext()) {
var adGroup = adgroups.next();
var adgroupName = adGroup.getName()
var adgroupNameL = adgroupName.toLowerCase()
if(adgroupNameL.includes(adgroupString)){
adGroup.pause()
changes.push([campaignName,adgroupName, "Paued"])
}
else if(!adgroupNameL.includes(ADGROUP_TO_NOT_ENABLE)){
adGroup.enable()
changes.push([campaignName,adgroupName, "Enabled"])
}
}
}
}
return changes
}
function arrayToTable(array) {
let table = '<table style="border: 1px solid black;">';
for (let i = 0; i < array.length; i++) {
let row = '<tr>';
for (let j = 0; j < array[i].length; j++) {
if (i === 0) {
row += '<th style="border: 1px solid black;">' + array[i][j] +
'</th>';
} else {
row += '<td style="border: 1px solid black;">' + array[i][j] +
'</td>';
}
}
row += '</tr>';
table += row;
}
table += '</table>';
return table;
}