The AdWords API will sunset on April 27, 2022. Migrate to the Google Ads API to take advantage of the latest Google Ads features.

Migration Samples

The code samples below provide examples of common migration functions using the AdWords API. Client Library.

#!/usr/bin/env ruby
# Encoding: utf-8
#
# Copyright:: Copyright 2015, Google Inc. All Rights Reserved.
#
# License:: 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.
#
# This example migrates your feed based sitelinks at campaign level to use
# extension settings. To learn more about extensionsettings, see
# https://developers.google.com/adwords/api/docs/guides/extension-settings.
# To learn more about migrating Feed based extensions to extension settings,
# see
# https://developers.google.com/adwords/api/docs/guides/migrate-to-extension-settings.
#
# This code example doesn't migrate scheduling or feeditem-level campaign,
# ad group, keyword, or geo targeting settings.

require 'adwords_api'
require 'set'

def migrate_to_extension_settings()
  # AdwordsApi::Api will read a config file from ENV['HOME']/adwords_api.yml
  # when called without parameters.
  adwords = AdwordsApi::Api.new

  # To enable logging of SOAP requests, set the log_level value to 'DEBUG' in
  # the configuration file or provide your own logger:
  # adwords.logger = Logger.new('adwords_xml.log')

  # Get all of the feeds for the current user.
  feeds = get_feeds(adwords)

  feeds.each do |feed|
    # Retrieve all the sitelinks from the current feed.
    feed_items = get_site_links_from_feed(adwords, feed)

    # Get all the instances where a sitelink from this feed has been added
    # to a campaign.
    campaign_feeds = get_campaign_feeds(adwords, feed, PLACEHOLDER_SITELINKS)

    all_feed_items_to_delete = campaign_feeds.map do |campaign_feed|
      # Retrieve the sitelinks that have been associated with this campaign.
      feed_item_ids = get_feed_item_ids_for_campaign(campaign_feed)

      if feed_item_ids.empty?
        puts(("Migration skipped for campaign feed with campaign ID %d " +
            "and feed ID %d because no mapped feed item IDs were found in " +
            "the campaign feed's matching function.") %
            [campaign_feed[:campaign_id], campaign_feed[:feed_id]])
        next
      end

      platform_restrictions = get_platform_restrictions(campaign_feed)

      # Delete the campaign feed that associates the sitelinks from the
      # feed to the campaign.
      delete_campaign_feed(adwords, campaign_feed)

      # Create extension settings instead of sitelinks.
      create_extension_setting(adwords, feed_items, campaign_feed,
          feed_item_ids, platform_restrictions)

      # Mark the sitelinks from the feed for deletion.
      feed_item_ids
    end.flatten.to_set.reject {|id| id.nil?}

    # Delete all the sitelinks from the feed.
    delete_old_feed_items(adwords, all_feed_items_to_delete, feed)
  end
end

def get_site_links_from_feed(adwords, feed)
  # Retrieve the feed's attribute mapping.
  feed_mappings = get_feed_mapping(adwords, feed, PLACEHOLDER_SITELINKS)

  feed_items = {}

  get_feed_items(adwords, feed).each do |feed_item|
    site_link_from_feed = {}

    feed_item[:attribute_values].each do |attribute_value|
      # Skip this attribute if it hasn't been mapped to a field.
      next unless feed_mappings.has_key?(
          attribute_value[:feed_attribute_id])

      feed_mappings[attribute_value[:feed_attribute_id]].each do |field_id|
        case field_id
        when PLACEHOLDER_FIELD_SITELINK_LINK_TEXT
          site_link_from_feed[:text] = attribute_value[:string_value]
        when PLACEHOLDER_FIELD_SITELINK_URL
          site_link_from_feed[:url] = attribute_value[:string_value]
        when PLACEHOLDER_FIELD_FINAL_URLS
          site_link_from_feed[:final_urls] = attribute_value[:string_values]
        when PLACEHOLDER_FIELD_FINAL_MOBILE_URLS
          site_link_from_feed[:final_mobile_urls] =
              attribute_value[:string_values]
        when PLACEHOLDER_FIELD_TRACKING_URL_TEMPLATE
          site_link_from_feed[:tracking_url_template] =
              attribute_value[:string_value]
        when PLACEHOLDER_FIELD_LINE_2_TEXT
          site_link_from_feed[:line2] = attribute_value[:string_value]
        when PLACEHOLDER_FIELD_LINE_3_TEXT
          site_link_from_feed[:line3] = attribute_value[:string_value]
        end
      end
    end

    feed_items[feed_item[:feed_item_id]] = site_link_from_feed
  end
  return feed_items
end

def get_feed_mapping(adwords, feed, placeholder_type)
  feed_mapping_srv = adwords.service(:FeedMappingService, API_VERSION)
  query = ("SELECT FeedMappingId, AttributeFieldMappings " +
      "WHERE FeedId = %d AND PlaceholderType = %d AND Status = 'ENABLED'") %
      [feed[:id], placeholder_type]

  attribute_mappings = {}
  offset = 0

  begin
    page_query = (query + " LIMIT %d, %d") % [offset, PAGE_SIZE]
    page = feed_mapping_srv.query(page_query)

    unless page[:entries].nil?
      # Normally, a feed attribute is mapped only to one field. However, you
      # may map it to more than one field if needed.
      page[:entries].each do |feed_mapping|
        feed_mapping[:attribute_field_mappings].each do |attribute_mapping|
          # Since attribute_mappings can have multiple values for each key,
          # we set up an array to store the values.
          if attribute_mappings.has_key?(attribute_mapping[:feed_attribute_id])
            attribute_mappings[attribute_mapping[:feed_attribute_id]] <<
                attribute_mapping[:field_id]
          else
            attribute_mappings[attribute_mapping[:feed_attribute_id]] =
                [attribute_mapping[:field_id]]
          end
        end
      end
    end
    offset += PAGE_SIZE
  end while page[:total_num_entries] > offset

  return attribute_mappings
end

def get_feeds(adwords)
  feed_srv = adwords.service(:FeedService, API_VERSION)
  query = "SELECT Id, Name, Attributes " +
      "WHERE Origin = 'USER' AND FeedStatus = 'ENABLED'"

  feeds = []
  offset = 0

  begin
    page_query = (query + " LIMIT %d, %d") % [offset, PAGE_SIZE]
    page = feed_srv.query(page_query)

    unless page[:entries].nil?
      feeds += page[:entries]
    end
    offset += PAGE_SIZE
  end while page[:total_num_entries] > offset

  return feeds
end

def get_feed_items(adwords, feed)
  feed_item_srv = adwords.service(:FeedItemService, API_VERSION)
  query = ("SELECT FeedItemId, AttributeValues " +
      "WHERE Status = 'ENABLED' AND FeedId = %d") % feed[:id]

  feed_items = []
  offset = 0

  begin
    page_query = (query + " LIMIT %d, %d") % [offset, PAGE_SIZE]
    page = feed_item_srv.query(page_query)

    unless page[:entries].nil?
      feed_items += page[:entries]
    end
    offset += PAGE_SIZE
  end while page[:total_num_entries] > offset

  return feed_items
end

def get_platform_restrictions(campaign_feed)
  platform_restrictions = nil

  if campaign_feed[:matching_function][:operator] == 'AND'
    campaign_feed[:matching_function][:lhs_operand].each do |argument|
      # Check if matchingFunction is EQUALS(CONTEXT.DEVICE, 'Mobile')
      if argument[:value][:operator] == 'EQUALS'
        request_context_operand = argument[:value][:lhs_operand].first()
        if request_context_operand &&
            request_context_operand == 'DEVICE_PLATFORM'
          platform_restrictions =
              argument[:value][:rhs_operand].first().upcase()
          break
        end
      end
    end
  end
  return platform_restrictions
end

def delete_old_feed_items(adwords, feed_item_ids, feed)
  return if feed_item_ids.empty?

  feed_item_srv = adwords.service(:FeedItemService, API_VERSION)

  operations = feed_item_ids.map do |feed_item_id|
    {
      :operator => 'REMOVE',
      :operand => {
        :feed_id => feed[:id],
        :feed_item_id => feed_item_id
      }
    }
  end

  feed_item_srv.mutate(operations)
end

def create_extension_setting(
    adwords, feed_items, campaign_feed, feed_item_ids, platform_restrictions)
  campaign_extension_setting_srv = adwords.service(
      :CampaignExtensionSettingService, API_VERSION)

  extension_feed_items = feed_item_ids.map do |feed_item_id|
    site_link_from_feed = feed_items[:feed_item_id]
    site_link_feed_item = {
      :sitelink_text => site_link_from_feed[:text],
      :sitelink_line2 => site_link_from_feed[:line2],
      :sitelink_line3 => site_link_from_feed[:line3]
    }
    if !site_link_from_feed.final_urls.nil? &&
        site_link_from_feed[:final_urls].length > 0
      site_link_feed_item[:sitelink_final_urls] = {
        :urls => site_link_from_feed[:final_urls]
      }
      unless site_link_from_feed[:final_mobile_urls].nil?
        site_link_feed_item[:sitelink_final_mobile_urls] = {
          :urls => site_link_from_feed[:final_mobile_urls]
        }
      end
      site_link_feed_item[:sitelink_tracking_url_template] =
          site_link_from_feed[:tracking_url_template]
    else
      site_link_feed_item[:sitelink_url] = site_link_from_feed[:url]
    end

    site_link_feed_item
  end

  extension_setting = {
    :extensions => extension_feed_items
  }

  unless platform_restrictions.nil?
    extension_setting[:platform_restrictions] = platform_restrictions
  end

  campaign_extension_setting = {
    :campaign_id => campaign_feed[:campaign_id],
    :extension_type => 'SITELINK',
    :extension_setting => extension_setting
  }

  operation = {
    :operand => campaign_extension_setting,
    :operator => 'ADD'
  }

  campaign_extension_setting_srv.mutate([operation])
end

def delete_campaign_feed(adwords, campaign_feed)
  campaign_feed_srv = adwords.service(:CampaignFeedService, API_VERSION)

  operation = {
    :operand => campaign_feed,
    :operator => 'REMOVE'
  }

  campaign_feed_srv.mutate([operation])
end

def get_feed_item_ids_for_campaign(campaign_feed)
  feed_item_ids = Set.new
  if !campaign_feed[:matching_function][:lhs_operand].empty? &&
      campaign_feed[:matching_function][:lhs_operand].first[:xsi_type] ==
      'RequestContextOperand'
    request_context_operand =
        campaign_feed[:matching_function][:lhs_operand].first
    if request_context_operand[:context_type] == 'FEED_ITEM_ID' &&
        campaign_feed[:matching_function][:operator] == 'IN'
      campaign_feed[:matching_function][:rhs_operand].each do |argument|
        if argument[:xsi_type] == 'ConstantOperand'
          feed_item_ids.add(argument[:long_value])
        end
      end
    end
  end
  return feed_item_ids
end

def get_campaign_feeds(adwords, feed, placeholder_type)
  campaign_feed_srv = adwords.service(:CampaignFeedService, API_VERSION)
  query = ("SELECT CampaignId, MatchingFunction, PlaceholderTypes " +
      "WHERE Status = 'ENABLED' AND FeedId = %d " +
      "AND PlaceholderTypes CONTAINS_ANY [%d]") % [feed[:id], placeholder_type]

  campaign_feeds = []
  offset = 0

  begin
    page_query = (query + " LIMIT %d, %d") % [offset, PAGE_SIZE]
    page = campaign_feed_srv.query(page_query)

    unless page[:entries].nil?
      campaign_feeds += page[:entries]
    end
    offset += PAGE_SIZE
  end while page[:total_num_entries] > offset

  return campaign_feeds
end

if __FILE__ == $0
  API_VERSION = :v201809
  PAGE_SIZE = 500

  # See the Placeholder reference page for a liste of all placeholder types
  # and fields.
  # https://developers.google.com/adwords/api/docs/appendix/placeholders
  PLACEHOLDER_SITELINKS = 1
  PLACEHOLDER_FIELD_SITELINK_LINK_TEXT = 1
  PLACEHOLDER_FIELD_SITELINK_URL = 2
  PLACEHOLDER_FIELD_LINE_2_TEXT = 3
  PLACEHOLDER_FIELD_LINE_3_TEXT = 4
  PLACEHOLDER_FIELD_FINAL_URLS = 5
  PLACEHOLDER_FIELD_FINAL_MOBILE_URLS = 6
  PLACEHOLDER_FIELD_TRACKING_URL_TEMPLATE = 7

  begin
    migrate_to_extension_settings()

  # Authorization error.
  rescue AdsCommon::Errors::OAuth2VerificationRequired => e
    puts "Authorization credentials are not valid. Edit adwords_api.yml for " +
        "OAuth2 client ID and secret and run misc/setup_oauth2.rb example " +
        "to retrieve and store OAuth2 tokens."
    puts "See this wiki page for more details:\n\n  " +
        'https://github.com/googleads/google-api-ads-ruby/wiki/OAuth2'

  # HTTP errors.
  rescue AdsCommon::Errors::HttpError => e
    puts "HTTP Error: %s" % e

  # API errors.
  rescue AdwordsApi::Errors::ApiException => e
    puts "Message: %s" % e.message
    puts 'Errors:'
    e.errors.each_with_index do |error, index|
      puts "\tError [%d]:" % (index + 1)
      error.each do |field, value|
        puts "\t\t%s: %s" % [field, value]
      end
    end
  end
end