Этот скрипт позволяет вам координировать использование модификаторов ставок в ставках вашей кампании с заранее определенным графиком в электронной таблице.
Типичный вариант использования — координация повышения ставок с рекламными кампаниями на телевидении или серией запланированных спортивных мероприятий.
Как работает скрипт
Скрипт работает путем чтения электронной таблицы, в которой заранее указаны даты и время периодов подъема. Точный модификатор ставки и продолжительность повышения можно указать в электронной таблице или установить глобально.
Скрипт запускается каждый час для обновления расписаний кампаний. Таблицу можно обновить в любое время, и изменения отразятся при следующем запуске сценария.
Предостережения
- Режим ставок
- Кампании, использующие автоматическое назначение ставок, могут не работать с этим скриптом, поскольку они могут быть несовместимы с расписаниями показа объявлений. См. возможность корректировки ставок .
- Существующее использование расписаний показа объявлений
- Если кампания опирается на уже существующие расписания показов объявлений или другие сценарии, основанные на настройке расписаний показа объявлений, ее не следует использовать с этим сценарием.
- Диапазоны настроек
- Изменение ставки может варьироваться от -90 до +900 процентов. Продолжительность подъема может составлять от 15 до 180 минут.
- Часовой пояс
- Используется часовой пояс аккаунта. Вам следует проверить, настроена ли учетная запись для распознавания летнего времени.
Настраивать
Создайте в своем аккаунте Google Рекламы ярлык под названием
TV Scheduled
.Примените этот ярлык к кампаниям, для которых вы хотите корректировать модификаторы ставок по расписанию.
Создайте электронную таблицу для хранения информации о расписании. Вы можете использовать этот шаблон или просмотреть настройку электронной таблицы , если у вас уже есть электронная таблица.
Создайте скрипт в своем аккаунте Google Рекламы.
Добавьте ссылку на свою таблицу, заменив
INSERT_SPREADSHEET_URL_HERE
URL-адресом своей таблицы.Замените
INSERT_EMAIL_ADDRESS_HERE
своим адресом электронной почты, чтобы получать уведомления о любых ошибках.
Тестирование
Нажмите «Предварительный просмотр» , чтобы запустить сценарий. В случае успеха он должен распечатать даты из вашей электронной таблицы в окне журнала. Если это не помогло, см. раздел Настройка электронной таблицы . Также проверьте указанную вами учетную запись электронной почты, так как туда отправляется информация о любых ошибках.
Повторяйте предварительный просмотр, пока не убедитесь, что даты читаются правильно.
Бег в прямом эфире
Запланируйте выполнение сценария каждый час .
Для правильной обработки кампаний важно запускать скрипт ежечасно.
Удаление
Если вы решите прекратить использование скрипта, выполните следующие действия, чтобы кампании вернулись к нормальной работе:
Измените строку в скрипте, которая гласит
var UNINSTALL = false;
прочитатьvar UNINSTALL = true;
Запустите ( а не просто просмотрите ) сценарий.
Удалите почасовое расписание для сценария.
Настройка электронной таблицы
Доступен шаблон электронной таблицы , но вы также можете использовать свою собственную.
Если вы не используете шаблон, обязательно назовите лист в своей электронной таблице: Schedule
.
Конфигурация электронной таблицы находится в скрипте и выглядит следующим образом:
const SPREADSHEET_CONFIG = {
hasHeader: true,
date: 'A',
bidModifier: 'B',
duration: 'C'
};
Поле hasHeader
указывает, следует ли пропустить первую строку. Остальные значения ссылаются на столбец электронной таблицы, в котором можно найти значение. При изменении этих значений на другие столбцы, например D
или E
, выбирается другой столбец.
Указание фиксированных значений
Если в вашей таблице нет модификатора ставки или продолжительности, вы можете исправить эти значения, используя знак равенства ( =
). Например, в этой конфигурации модификатор ставки +50 процентов для всех записей в электронной таблице:
const SPREADSHEET_CONFIG = {
hasHeader: true,
date: 'A',
bidModifier: '=1.5',
duration: 'C'
};
В этом примере также указана фиксированная продолжительность подъема, равная 30 минутам:
const SPREADSHEET_CONFIG = {
hasHeader: true,
date: 'A',
bidModifier: '=1.5',
duration: '=30'
};
Даты устранения неполадок
Могут работать разные форматы даты и времени, но рекомендуемым выбором является YYYY/mm/DD HH:MM
.
Вот несколько советов, как правильно настроить даты:
При копировании из другого источника, например из электронной таблицы Excel или CSV, используйте параметры этого инструмента для однозначного форматирования дат перед копированием и вставкой.
Проверьте, распознал ли Таблицы значение как дату, дважды щелкнув значение, чтобы отредактировать его: если это дата, появится средство выбора даты.
Используйте предварительный просмотр сценария, пока не убедитесь, что даты считываются надежно.
Подробное объяснение скрипта
В сценарии широко используются расписания показа рекламы. Расписания объявлений в Google Ads обладают следующими свойствами:
- До шести элементов расписания для каждой кампании в день.
- Элементы расписания устанавливаются по дням (понедельник-воскресенье), а не по дате.
- Детализация времени составляет 15 минут, поэтому элементы расписания могут начинаться в час или через 15, 30 или 45 минут.
Используйте следующий подход, чтобы установить модификаторы ставок, которые не соответствуют повторяющемуся шаблону с понедельника по воскресенье или для которых может потребоваться более шести различных периодов повышения ставок для определенного дня:
- Каждый час скрипт считывает таблицу.
- Читаются только элементы, относящиеся к следующим двум часам.
- Все существующие элементы расписания на этот день для этой кампании будут удалены.
- Добавлены элементы расписания, охватывающие следующие два часа.
Таким образом, для корректировки модификаторов ставок можно использовать произвольный график.
Скрипт обрабатывает детализацию времени следующим образом:
- Если начало элемента в электронной таблице не попадает в 15-минутную границу, оно округляется назад .
- Если конец элемента в электронной таблице не попадает на 15-минутную границу, он округляется вперед .
Скрипт использует следующий подход для модификаторов ставок:
- Если в таблице есть перекрывающиеся элементы, приоритет имеет элемент с наибольшим модификатором ставки.
Исходный код
// 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 TV Advert bid coordination.
*
* @overview Allows Google Ads and TV-based advert schedules to be coordinated.
* Visit
* https://developers.google.com/google-ads/scripts/docs/solutions/tv-schedule
* for more details.
*
* @author Google Ads Scripts Team [adwords-scripts@googlegroups.com]
*
* @version 2.0
*
* @changelog
* - version 2.0
* - Updated to use new Google Ads scripts features.
* - version 1.1.1
* - Added validation for external spreadsheet setup.
* - version 1.1
* - Fixed minor bug with error messages.
* - version 1.0
* - Released initial version.
*/
// The URL for the spreadsheet of TV advert dates and times. This spreadsheet
// can be used as a template:
//
// https://docs.google.com/spreadsheets/d/1Ps3x3cQ7ixBysVfDS8VObbKofcVrOJtXaWvk4eMjAeA
//
const SPREADSHEET_URL = 'INSERT_SPREADSHEET_URL_HERE';
// Email addresses for notification of any errors encountered.
const EMAIL_RECIPIENTS = ['INSERT_EMAIL_ADDRESS_HERE'];
// Spreadsheet configuration.
const SPREADSHEET_CONFIG = {
hasHeader: true,
date: 'A',
bidModifier: 'B',
duration: 'C'
};
// Flag to set for uninstalling the script. Instructions at
//
// https://developers.google.com/google-ads/scripts/docs/solutions/tv-schedule
//
const UNINSTALL = false;
// The label applied to all TV scheduled campaigns.
const TV_CAMPAIGN_LABEL = 'TV Scheduled';
const DAYS = [
'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY'
];
const MILLIS_PER_MIN = 1000 * 60;
const MILLIS_PER_DAY = MILLIS_PER_MIN * 60 * 24;
const SLOT_MINS = 15;
const SLOTS_PER_HOUR = 60 / SLOT_MINS;
const SLOTS_PER_DAY = 60 * 24 / SLOT_MINS;
// The Google Ads limit on the number schedule entries per campaign per day.
const MAX_SCHEDULE_ITEMS_PER_DAY = 6;
const MIN_BID_MODIFIER = 0.1;
const MAX_BID_MODIFIER = 9.0;
const MIN_DURATION_MINS = 0;
const MAX_DURATION_MINS = 180;
const ROUND_FORWARD = 1;
const ROUND_BACKWARD = 0;
// The 'range' specifies how far ahead, when setting schedule items to affect
// the immediate future, items should be inserted for.
const DEFAULT_RANGE = MAX_SCHEDULE_ITEMS_PER_DAY * SLOT_MINS;
// Only manual bidding strategies support Ad Scheduling bid adjustments.
const MANUAL_BIDDING_STRATEGIES = ['MANUAL_CPC', 'MANUAL_CPC'];
const SCHEDULE_SHEET_NAME = 'Schedule';
// Variables used to hold Dates for the three days relevant to this execution.
let yesterday = null;
let today = null;
let tomorrow = null;
/**
* Main entry point
*/
function main() {
let execDateTime = SimpleDate.fromDate(
localDate(new Date(), AdsApp.currentAccount().getTimeZone()));
const tvScheduleCreator = new TvScheduleCreator();
const spreadsheetReader = new SpreadsheetReader(
SPREADSHEET_URL, SPREADSHEET_CONFIG);
const errors = spreadsheetReader.getErrors();
if (UNINSTALL) {
removeAllSchedulesForUninstall(errors);
} else {
// Form relevant dates.
today = SimpleDate.fromDate(new Date());
yesterday = SimpleDate.fromDate(getDatePlusDays(new Date(), -1));
tomorrow = SimpleDate.fromDate(getDatePlusDays(new Date(), 1));
// Only add items from the spreadsheet if no errors were encountered.
if (errors.length === 0) {
const rows = spreadsheetReader.getRows();
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
tvScheduleCreator.addTvAd(row[0], row[1], row[2]);
}
}
let scheduleItems = tvScheduleCreator.getScheduleItemsToAdd(
execDateTime);
const campaigns = getCampaigns();
for (let i = 0; i < campaigns.length; i++) {
const campaign = campaigns[i];
const biddingStrategy = campaign.bidding().getStrategyType();
if (MANUAL_BIDDING_STRATEGIES.indexOf(biddingStrategy) >= 0) {
applySchedulesToCampaign(campaign, scheduleItems);
} else {
storeAndLogError(errors, `"${campaign.getName()}"` +
` does not use manual bidding, so cannot use Ad Scheduling. ` +
`Visit https://support.google.com/google-ads/answer/2732132`);
}
}
}
if (errors.length > 0) {
validateEmailAddresses();
sendErrorEmail(errors);
}
}
/**
* Returns a Date formatted for the specified timezone.
*
* @param {Date} date The date to convert.
* @param {String} timeZone The desired time zone.
* @return {!Date} The newly-constructed Date.
*/
function localDate(date, timeZone) {
return new Date(Utilities.formatDate(date, timeZone, 'yyyy MMM dd HH:mm:ss'));
}
/**
* Creates a Date +/- a given number of days from the specified Date.
*
* @param {Date} date The date to start from.
* @param {number} days The number of days to add (or subtract if negative).
* @return {!Date} The newly created Date.
*/
function getDatePlusDays(date, days) {
const newDate = new Date(date.getTime());
newDate.setTime(date.getTime() + days * MILLIS_PER_DAY);
return newDate;
}
/**
* SimpleDate represents a date/time with granularity of 15 mins.
*/
class SimpleDate {
/**
* @param {number} year Year e.g. 2015.
* @param {number} month 0-indexed month, e.g. 0 = January.
* @param {number} day 1-31 day of the month.
* @param {number} hours 0-23
* @param {number} minutes 0-59, will be rounded to multiple of 15.
* @param {number} roundingMode Whether to round back (default) or forward.
*/
constructor (year, month, day, hours, minutes, roundingMode) {
let roundedMins = null;
if (roundingMode === ROUND_FORWARD) {
roundedMins = Math.ceil(minutes / SLOT_MINS) * SLOT_MINS;
} else {
roundedMins = Math.floor(minutes / SLOT_MINS) * SLOT_MINS;
}
this.datetime = new Date(year, month, day, hours, roundedMins);
}
/**
* Returns the slot that this time falls in (for example, between 0 and 95).
*
* @return {number} The 15-min slot that the given time falls into.
*/
getSlotIndex() {
return this.datetime.getHours() * SLOTS_PER_HOUR +
Math.floor(this.datetime.getMinutes() / SLOT_MINS);
}
/**
* Returns the day of week represented in numbers.
*
* @return {number} The day of the week represented in numbers.
*/
getDayOfWeek() {
return this.datetime.getDay();
}
/**
* Returns the hour that this time falls in.
*
* @return {number} The hour that the given time falls into.
*/
getHours() {
return this.datetime.getHours();
}
/**
* Returns the minute that this time falls in.
*
* @return {number} The minute that the given time falls into.
*/
getMinutes() {
return this.datetime.getMinutes();
}
/**
* Returns true if .
*
* @param {!Date} simpleDate The given date.
* @return {boolean} Whether this constraint is met or not.
*/
equals(simpleDate) {
return this.valueOf() === simpleDate.valueOf();
}
/**
* Returns the time divided by millisecond per minute.
*
* @return {number} The time divided by millisecond per minute.
*/
valueOf() {
return this.datetime.getTime() / MILLIS_PER_MIN;
}
/**
* Returns the date and time in desired string format.
*
* @return {string} The date and time in desired format.
*/
toString() {
return Utilities.formatString('%d-%02d-%02d %02d:%02d',
this.datetime.getFullYear(), this.datetime.getMonth() + 1,
this.datetime.getDate(), this.datetime.getHours(),
this.datetime.getMinutes());
}
/**
* Returns the date and time in desired string format.
*
* @param {!object} simpleTime The datetime object
* @return {boolean} Whether the constraint is met or not.
*/
isSameDay(simpleTime) {
return this.datetime.getFullYear() === simpleTime.datetime.getFullYear() &&
this.datetime.getMonth() === simpleTime.datetime.getMonth() &&
this.datetime.getDate() === simpleTime.datetime.getDate();
}
/**
* Creates a SimpleDate from a given Date object
*
* @param {Date} date The date to convert.
* @param {number} roundingMode Whether to round forward or back.
* @return {SimpleDate} The new SimpleDate, or null if not a Date or invalid.
*/
static fromDate(date, roundingMode) {
if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
return null;
}
return new SimpleDate(date.getFullYear(), date.getMonth(), date.getDate(),
date.getHours(), date.getMinutes(), roundingMode);
}
/**
* Creates a SimpleDate from the given SimpleDate and slot index.
*
* @param {SimpleDate} simpleDate The SimpleDate to copy year/month/day from.
* @param {number} index The 15-min slot index (e.g. 0-95) to represent time.
* @return {!SimpleDate} The new SimpleDate.
*/
static fromSimpleDateIndex(simpleDate, index) {
return new SimpleDate(simpleDate.datetime.getFullYear(),
simpleDate.datetime.getMonth(), simpleDate.datetime.getDate(),
Math.floor(index / SLOTS_PER_HOUR),
index % SLOTS_PER_HOUR * SLOT_MINS);
}
}
/**
* Provides the ability to read TV advert details from a Google Spreadsheet.
*/
class SpreadsheetReader {
/**
* Reads a Google Sheet, to obtain the TV ad schedules.
*
* @param {string} url The URL of the spreadsheet.
* @param {object} config Configuration of which fields are in which columns.
*/
constructor (url, config) {
this.errors = [];
let rawData = [];
try {
rawData = SpreadsheetReader.readDataFromGoogleSheet_(url);
} catch (e) {
storeAndLogError(this.errors, e.message);
}
if (rawData) {
this.data = SpreadsheetReader.parseData_(this.errors, rawData, config);
}
}
/**
* Retrieves successfully parsed entries.
*
* @return {object[]} The rows
* @return {Date} object[][0] The start date for the ad.
* @return {number} object[][1] The bid modifier.
* @return {number} object[][2] The duration in minutes.
*/
getRows() {
return this.data;
}
/**
* Retrieves errors encountered in loading data from the spreadsheet.
*
* @return {!String[]} The list of errors.
*/
getErrors() {
return this.errors;
}
/**
* Reads a two-dimensional list representing the Google Spreadsheet.
* Individual cells can vary in their data type, being String, number or Date.
*
* @param {string} url The spreadsheet URL.
* @return {object[][]} 2D representation of the data in the file.
* @private
*/
static readDataFromGoogleSheet_(url) {
const spreadsheet = validateAndGetSpreadsheet(url);
if (!spreadsheet) {
throw Error(`Unable to open spreadsheet with URL: ${url}`);
}
const timeZone = spreadsheet.getSpreadsheetTimeZone();
const sheet = spreadsheet.getSheetByName(SCHEDULE_SHEET_NAME);
if (!sheet) {
throw Error(`Spreadsheet must have a sheet named: "` +
`${SCHEDULE_SHEET_NAME}"`);
}
const values = sheet.getDataRange().getValues();
for (let i = 0; i < values.length; i++) {
for (let j = 0; j < values[i].length; j++) {
if (values[i][j] instanceof Date) {
values[i][j] = localDate(values[i][j], timeZone);
}
}
}
return values;
}
/**
* Determines whether an TV ad satisfies constraints.
*
* @param {string} errors An array to store errors encountered.
* @param {Date} date Represents the start date/time.
* @param {number} bidModifier The bid modifier for the period.
* @param {number} duration The duration of the modifier, in minutes.
* @param {number} rowIndex The row number, counting from 1.
* @return {boolean} Whether these constraints are met.
* @private
*/
static isValidEntry_(errors, date, bidModifier, duration, rowIndex) {
if (AdsApp.getExecutionInfo().isPreview()) {
console.log([
`Row: ${rowIndex}`,
`startDate (y-m-d h-m): ${SimpleDate.fromDate(date)}`,
`bidModifier: ${bidModifier}`,
`duration: ${duration}`
].join(' '));
}
if (bidModifier < MIN_BID_MODIFIER || bidModifier > MAX_BID_MODIFIER ||
isNaN(bidModifier)) {
storeAndLogError(errors, `Invalid bid modifier at row ${rowIndex}`);
return false;
} else if (duration <= MIN_DURATION_MINS || duration > MAX_DURATION_MINS) {
storeAndLogError(errors, `Invalid duration at row ${rowIndex}`);
return false;
} else if (!date || !(date instanceof Date) || isNaN(date.getTime())) {
storeAndLogError(errors, `Invalid date/time at row ${rowIndex}`);
return false;
}
return true;
}
/**
* Converts an alphabet-based column index to a numeric one, e.g. "A" -> 0.
* @param {string} alpha The single character to convert.
* @return {number} The numerical representation.
* @private
*/
static alphaToCol_(alpha) {
return alpha.charCodeAt() - 'A'.charCodeAt();
}
/**
* Parses the raw data obtained from a Google Spreadsheet to obtain triples of
* start date/time, bid modifier and duration.
*
* @param {string} errors An array to store errors encountered.
* @param {!object} rawData 2D list of the raw data obtained from the file.
* @param {object} config Configuration of which fields are in which columns.
* @return {!object} The parsed row data.
* @private
*/
static parseData_(errors, rawData, config) {
let parsedRows = [];
for (let r = config.hasHeader ? 1 : 0; r < rawData.length; r++) {
let date = null;
let bidModifier = null;
let duration = null;
const row = rawData[r];
// '=' prefix means fixed value, otherwise the value specifies the column.
if (config.bidModifier[0] === '=') {
bidModifier = config.bidModifier.substring(1).replace(',', '.');
} else {
bidModifier = parseFloat(
String(row[SpreadsheetReader.alphaToCol_(
config.bidModifier)]).replace(',', '.'));
}
if (config.duration[0] === '=') {
duration = config.duration.substring(1);
} else {
duration = row[SpreadsheetReader.alphaToCol_(config.duration)];
}
if (config.date[0] === '=') {
date = new Date(config.date.substring(1));
} else {
date = row[SpreadsheetReader.alphaToCol_(config.date)];
}
if (SpreadsheetReader.isValidEntry_(
errors, date, bidModifier, duration, r + 1)) {
parsedRows.push([date, bidModifier, duration]);
}
}
return parsedRows;
}
}
/**
* Creates a list of Google Ads schedule items based on TV ad times and details.
*/
class TvScheduleCreator {
/**
* Creates a TV schedule object. This is used for calculating which Google Ads
* schedule items to create for a given set of TV ad details.
*/
constructor () {
this.daySlots = [];
for (let d = 0; d < DAYS.length; d++) {
let slots = [];
for (let i = 0; i < SLOTS_PER_DAY; i++) {
slots.push(null);
}
this.daySlots.push(slots);
}
}
/**
* Retrieves the schedule items that should be added to campaigns.
*
* @param {SimpleDate} curDate The date/time of current execution.
* @param {number=} opt_range The range beyond curDate deemed relevant. If not
* specified, defaults to DEFAULT_RANGE.
* @return {object[]} A list of schedule items for applying to campaigns.
*/
getScheduleItemsToAdd(curDate, opt_range) {
const todaySlots = this.daySlots[curDate.getDayOfWeek()];
let curModifier = todaySlots[0];
let start = 0;
let schedule = [];
let range = opt_range || DEFAULT_RANGE;
// If there is a change in bid modifer from the last slot to this then this
// represents the boundary of a schedule.
for (let index = 0; index < todaySlots.length; index++) {
if (todaySlots[index] !== curModifier) {
TvScheduleCreator.addIfInBounds_(
schedule, curDate, start, index, range, curModifier);
start = index;
curModifier = todaySlots[index];
}
}
TvScheduleCreator.addIfInBounds_(
schedule, curDate, start, SLOTS_PER_DAY, range, curModifier);
// If the range of relevant schedule items possibly extends to tomorrow, add
// results from that part of tomorrow too.
if (SLOTS_PER_DAY - curDate.getSlotIndex() < range / SLOT_MINS) {
range -= (SLOTS_PER_DAY - curDate.getSlotIndex()) * SLOT_MINS;
const nextDay = getDatePlusDays(curDate.datetime, 1);
nextDay.setHours(0);
nextDay.setMinutes(0);
const tomorrowFirstSlot = SimpleDate.fromDate(nextDay);
const extraResults = this.getScheduleItemsToAdd(tomorrowFirstSlot, range);
extraResults.forEach(function(entry) {
schedule.push(entry);
});
}
return schedule;
}
/**
* Adds the specified TV ad start/duration to the schedule. The modifier is
* only updated where the existing modifier is less, or not specified.
*
* @param {Date} start The start time of the TV advert.
* @param {Number} bidModifier The bid modifier to apply.
* @param {Number} duration The duration of the uplift, in minutes.
*/
addTvAd (start, bidModifier, duration) {
// Only TV ads scheduled for yesterday, today or tomorrow can be relevant.
const end = new Date(start.getTime() + duration * MILLIS_PER_MIN);
const simpleStart = SimpleDate.fromDate(start, ROUND_BACKWARD);
const simpleEnd = SimpleDate.fromDate(end, ROUND_FORWARD);
if (!simpleStart.isSameDay(yesterday) && !simpleStart.isSameDay(today) &&
!simpleStart.isSameDay(tomorrow)) {
return;
}
const todaySlots = this.daySlots[simpleStart.getDayOfWeek()];
const startIndex = simpleStart.getSlotIndex();
let endIndex = simpleEnd.getSlotIndex();
if (endIndex < startIndex) {
// Adds ads for tomorrow, if the duration takes it into the next day.
const tomorrowSlots = this.daySlots[(simpleStart.getDayOfWeek() + 1) % 7];
TvScheduleCreator.applyModifierToRange_(
tomorrowSlots, 0, endIndex, bidModifier);
endIndex = SLOTS_PER_DAY;
}
TvScheduleCreator.applyModifierToRange_(
todaySlots, startIndex, endIndex, bidModifier);
}
/**
* Adds a schedule item if the start or end overlap with the period between
* now and now + range.
*
* @param {!object} schedule The list of schedule items to add to.
* @param {SimpleDate} date The current date and time of execution.
* @param {number} startIdx The slot-index that this uplift starts at.
* @param {number} endIdx The slot-index 1 past the end of the uplift.
* @param {number} range Length of the period from now to consider in minutes.
* @param {number} modifier The bid modifier to apply.
* @private
*/
static addIfInBounds_(schedule, date, startIdx, endIdx, range, modifier) {
const start = SimpleDate.fromSimpleDateIndex(date, startIdx);
const end = SimpleDate.fromSimpleDateIndex(date, endIdx);
let endHours = end.getHours();
if (end.getHours() === 0 && end.getMinutes() === 0) {
endHours = 24;
}
if (start >= date && start - date < range || start < date && end > date) {
schedule.push({
dayOfWeek: DAYS[date.getDayOfWeek()],
startHour: start.getHours(),
startMinute: start.getMinutes(),
endHour: endHours,
endMinute: end.getMinutes(),
bidModifier: modifier === null ? 1 : modifier
});
}
}
/**
* Applies the bid modifier to the slots in the range where no modifier exists
* or the existing modifier is less.
*
* @param {!object} scheduleSlots List holding the 96 time slots of the day.
* @param {number} startIndex The starting index of the range.
* @param {number} endIndex The final slot in the range, plus one.
* @param {number} modifier The bid modifier to apply.
* @private
*/
static applyModifierToRange_(scheduleSlots, startIndex, endIndex,
modifier) {
for (let j = startIndex; j < endIndex; j++) {
if (scheduleSlots[j] === null || modifier > scheduleSlots[j]) {
scheduleSlots[j] = modifier;
}
}
}
}
/**
* Applies Google Ads schedule items to a given campaign, after first clearing
* existing items.
*
* @param {Campaign} campaign The campaign to apply to.
* @param {object[]} scheduleItems The schedule items to add.
*/
function applySchedulesToCampaign(campaign, scheduleItems) {
removePastScheduleDay(campaign, yesterday.getDayOfWeek());
removePastScheduleDay(campaign, today.getDayOfWeek());
removePastScheduleDay(campaign, tomorrow.getDayOfWeek());
for (let i = 0; i < scheduleItems.length; i++) {
campaign.addAdSchedule(scheduleItems[i]);
}
}
/**
* Sends an email of the errors that have been encountered in parsing the
* spreadsheet file, which are supplied as a list of strings
*
* @param {String[]} errors The errors to list in the email.
*/
function sendErrorEmail(errors) {
const emailDate = localDate(new Date(),
AdsApp.currentAccount().getTimeZone());
const footerStyle = 'color: #aaaaaa; font-style: italic;';
const scriptsLink = 'https://developers.google.com/google-ads/scripts/';
const subject = `[Errors] Google Ads TV Coordination Script: ${emailDate}`;
let htmlBody = '<html><body><p>Hello,</p>' +
'<p>A Google Ads script has run and these errors were encountered :<ul>';
for (let i = 0; i < errors.length; i++) {
htmlBody += `<li>${errors[i]}</li>`;
}
htmlBody += `</ul></p><p>Regards,</p>` +
`<span style="${footerStyle}">This email was automatically` +
`generated by <a href="${scriptsLink}">Google Ads Scripts</a>.<span>` +
`</body></html>`;
const body = 'Please enable HTML to view this email.';
const options = {htmlBody: htmlBody};
MailApp.sendEmail(EMAIL_RECIPIENTS, subject, body, options);
}
/**
* Retrieves a list of campaigns that have been targeted for TV scheduling.
*
* @return {Campaign[]}
*/
function getCampaigns() {
const campaignList = [];
const campaigns = AdsApp.campaigns().
withCondition(`LabelNames CONTAINS_ANY ["${TV_CAMPAIGN_LABEL}"]`).
withCondition('Status = "ENABLED"').get();
for (const campaign of campaigns) {
campaignList.push(campaign);
}
return campaignList;
}
/**
* Removes schedules for specified day
*
* @param {Campaign} campaign The campaign to remove schedule items from.
* @param {number} dayIndex The day in question (0=Sunday, 1=Monday) etc.
*/
function removePastScheduleDay(campaign, dayIndex) {
const schedules = campaign.targeting().adSchedules().get();
for (const schedule of schedules) {
if (schedule.getDayOfWeek() === DAYS[dayIndex]) {
schedule.remove();
}
}
}
/**
* Removes schedules for all Campaigns, for use when ceasing use of the script.
*
* @param {string[]} errors Array to hold errors encountered.
*/
function removeAllSchedulesForUninstall(errors) {
if (AdsApp.getExecutionInfo().isPreview()) {
storeAndLogError(errors, 'Please run uninstall again in non-preview mode.');
return;
}
const campaigns = getCampaigns();
for (let i = 0; i < campaigns.length; i++) {
const campaign = campaigns[i];
const schedules = campaign.targeting().adSchedules().get();
for (const schedule of schedules) {
schedule.remove();
}
}
}
/**
* Helper to both log and collect errors.
*
* @param {string[]} logArray The array to append entries to.
* @param {string} errorMessage The message to append.
*/
function storeAndLogError(logArray, errorMessage) {
console.error(errorMessage);
logArray.push(errorMessage);
}
/**
* DO NOT EDIT ANYTHING BELOW THIS LINE.
* Please modify your spreadsheet URL and email addresses at the top of the file
* only.
*/
/**
* Validates the provided spreadsheet URL and email address
* to make sure that they're 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 or email 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 email address to make sure it's not the default.
* Throws a descriptive error message if validation fails.
*
* @throws {Error} If the list of email addresses is still the default
*/
function validateEmailAddresses() {
if (EMAIL_RECIPIENTS &&
EMAIL_RECIPIENTS[0] == 'INSERT_EMAIL_ADDRESS_HERE') {
throw new Error('Please specify a valid email address.');
}
}