0% found this document useful (0 votes)
6 views11 pages

Bids by Weather

Uploaded by

jkeating1179
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
6 views11 pages

Bids by Weather

Uploaded by

jkeating1179
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 11

// Copyright 2015, Google Inc. All Rights Reserved.

//
// 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.
*/

// Register for an API key at http://openweathermap.org/appid


// and enter the key below.
const OPEN_WEATHER_MAP_API_KEY = 'PUT_API_KEY_HERE';

// Create a copy of https://goo.gl/A59Uuc and enter the URL below.


const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/1O-
sEv3I98w1smcZBLj5AzFWi6X7qau4RMYy6oJUsKuo/edit#gid=2';

// name of the sheet with storage


const STORAGE_SHEET_NAME = "storage"

//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]"];

// A cache to store the weather for locations already lookedup earlier.


const WEATHER_LOOKUP_CACHE = {};

/**
* 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);

// Convert the data into dictionaries for convenient usage.


const campaignMapping = buildCampaignRulesMapping(campaignRuleData);
const weatherConditionMapping =
buildWeatherConditionMapping(weatherConditionData);
const locationMapping = buildLocationMapping(geoMappingData);

// Apply the rules.


for (const campaignName in campaignMapping) {
var campaignChanges = applyRulesForCampaign(campaignName,
campaignMapping[campaignName],
locationMapping, weatherConditionMapping);
if(campaignChanges.length > 0){
changesMade = changesMade.concat(campaignChanges)
}
}

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,

// location for which this rule applies.


'location': rules[1],

// the weather condition (e.g. Sunny).


'condition': rules[2],

// bid modifier to be applied.


'adgroups': rules[3],
});
campaignMapping[campaignName] = campaignRules;
}
}
Logger.log('Campaign Mapping: %s', campaignMapping);
return campaignMapping;
}

/**
* 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,

// Temperature (e.g. 50 to 70)


'temperature': weatherCondition[1],
// Precipitation (e.g. below 70)
'precipitation': weatherCondition[2],

// Wind speed (e.g. above 5)


'wind': weatherCondition[3]
};
}
Logger.log('Weather condition mapping: %s', weatherConditionMapping);
return weatherConditionMapping;
}

/**
* 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];

// Evaluate the weather rules.


if (evaluateWeatherRules(weatherConditionRules, weather)) {
Logger.log('Matching Rule found: Campaign Name = %s, location = %s, ' +
'weatherName = %s,weatherRules = %s, noticed weather = %s.',
campaignRule.name, campaignRule.location,
weatherConditionName, weatherConditionRules, weather);

var changedAdgroups = changeAdgroups(campaignName, adgroup,


weatherConditionName);
campaignData = campaignData.concat(changedAdgroups)
}
}
return campaignData;
}

/**
* 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;

return evaluateMatchRules(weatherRules.temperature, temperature) &&


evaluateMatchRules(weatherRules.precipitation, precipitation) &&
evaluateMatchRules(weatherRules.wind, windspeed);
}

/**
* 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];

for (const rule of rules) {


if (rule(condition, value)) {
return true;
}
}
return false;
}

/**
* 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;
}

if (value < conditionParts[1]) {


return true;
}
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;
}

if (conditionParts[0] <= value && value <= conditionParts[2]) {


return true;
}
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());

// OpenWeatherMap's way of returning errors.


if (result.cod != 200) {
throw Utilities.formatString(
'Error returned by API: %s, Location searched: %s.',
response.getContentText(), location);
}
WEATHER_LOOKUP_CACHE[location] = result;
return result;
}

/**
* 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 getDates (){


var timezone = AdsApp.currentAccount().getTimeZone()
let options = { timeZone: timezone };

// Get today's date


let date = new Date();
let laDate = new Date(date.toLocaleString('en-US', options));
let todaysDate = laDate.toISOString().split('T')[0];

// Get the date after 2 days


let afterTwoDays = new Date();
afterTwoDays.setDate(afterTwoDays.getDate() + 2);
let laDateTwoDays = new Date(afterTwoDays.toLocaleString('en-US', options));
let dateAfterTwoDays = laDateTwoDays.toISOString().split('T')[0];

const dates = {today:todaysDate, afterTwoDays: dateAfterTwoDays }


return dates
}

function sendEmailMessage(changesMade, revertedChages){


var msg = ""
if(changesMade.length > 0){
msg += "<h3> Below changes were made today </h3>"
msg +=arrayToTable(changesMade)
}
if(revertedChages.length > 0){
msg += "<h3> Below changes were reverted today </h3>"
msg +=arrayToTable(revertedChages)
}
if(msg.length > 0){
Logger.log("Sending Email(s)");
MailApp.sendEmail({
to: EMAILS.join(","),
subject: "Changes Made From Weather Script",
htmlBody: msg,
});
}

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;
}

You might also like