This document describes the migration and sunset process for Feed-based extensions. Feed-based extensions will be migrated to new Asset-based extensions in batches, and will fully sunset in 2023.
Overview
The existing Feed-based extensions paradigm is deprecated in favor of Asset-based extensions. Asset-based extensions reduce the complexity of creating and managing extensions. Action is required in order to ensure that you maintain control over your ad extensions.
The term "Feed-based extension" refers to any of the resources or functions associated with the following services:
AdGroupExtensionSettingService
AdGroupFeedService
CampaignExtensionSettingService
CampaignFeedService
CustomerExtensionSettingService
CustomerFeedService
ExtensionFeedItemService
FeedItemTargetService
FeedItemService
FeedMappingService
FeedService
If both Asset-based and Feed-based extensions of a particular type are attached to the same customer, campaign, or ad group, the Asset-based extensions will be served. This only applies for entities at the same level. So for example, linking an asset to an ad group will prevent all feeds serving on that ad group, but other ad groups in the same campaign may continue serving feeds. Asset-based extensions will also be shown by default in the Google Ads UI.
If you have active extensions of both types, you may see a notification on the Extensions page in the Google Ads UI, similar to the screenshot below. "Legacy" extensions are Feed-based extensions, while "Upgraded" extensions are Asset-based.
Note that once you've created any Asset-based extensions, the view will default to "Upgraded" extensions.
Migration details
Feeds are migrated by copying their existing values into a new Asset-based extension instance. Existing Feed-based extensions will be automatically copied into Asset-based extensions on specific dates. All Asset-based extensions will have new, unique ID values.
We recommend performing the migration process yourself through the Google Ads API as outlined in this guide, as there will be no connection between Assets created during the automatic migration and the original Feeds.
Migration schedule
All existing Feed-based extensions will automatically migrate to Asset-based extensions in batches. Accounts that have been migrated will reject mutate calls for Feed-based entities.
Historical feed reports will continue to be available until August 2022. Feed-based extensions will no longer generate new data after the migration.
The automatic migration process will create Asset-based extensions equivalent to
the current Feed-based extensions and replace all occurrences of
AdGroupFeed
,
CampaignFeed
,
and CustomerFeed
with equivalent
AdGroupAsset
,
CampaignAsset
,
and CustomerAsset
. If the ad group, campaign,
or customer has already been attached to an Asset, the corresponding Feed will
be ignored.
Changes in Asset-based extensions
While Asset-based extensions provide the same functionality as the existing Feed-based extensions, some existing features have been removed, including matching functions and most targeting fields.
For targeting, the ad group, campaign, keyword, location, and mobile fields will be unavailable to Asset-based extensions.
Certain fields will be unavailable in some Asset-based extensions. The following fields have been removed and will not be automatically migrated:
- App: ad schedules
- Call: start time, end time
- Price: start time, end time, ad schedules
- Structured Snippet: start time, end time, ad schedules
All date-related fields are now in the "yyyy-MM-dd" format. You will not be able to specify a time of day in any date field in Asset-based extensions.
For Asset-based extensions going forward, we recommend using targeting features already available at the campaign and ad group level. For mobile preference, consider including mobile-friendly URLs for any links.
Matching functions will not be available to Asset-based extensions. During the automatic migration, only the following matching functions will be honored:
Matching Function | Migration Notes |
---|---|
EQUALS(FEED_ITEM_ID, 1234567)
|
Migrated as-is |
IN(FEED_ITEM_ID,{1234567,1234568,1234569})
|
Migrated as-is |
IDENTITY(false)
|
Migrated as excluded_parent_asset_field_types
|
IDENTITY(true)
|
Migrated with all linked feed items, except for a special case described below. |
AND(
|
Migrated as IN(FEED_ITEM_ID,{1234567,1234568,1234569})
|
AND(
|
Migrated as EQUALS(FEED_ITEM_ID,1234567)
|
IDENTITY(true) special case
An IDENTITY(true)
Matching Function will be ignored during automatic
migrations if it is attached to an extension and matches to more than 20 active
feed items. You should update the Matching Function to the
IN(FEED_ITEM_ID, {1234567,1234568,1234569})
notation.
Alternatively, you can self-migrate your extensions and link an Asset-based extension to other entities. Each customer, campaign, and ad group can be linked to up to 20 active Asset-based extensions of the same extension type. See the migration procedure for information on how to migrate and link to Asset-based extensions.
Call tracking considerations
The call_tracking_enabled
and call_conversion_tracking_disabled
fields have
been removed in CallAsset
. Call tracking is now
controlled at the account level using
CallReportingSetting
. Use the
CallAsset.call_conversion_reporting_state
field to enable conversion tracking
on Call extensions. call_conversion_reporting_state
can be set to one of the
following values:
DISABLED
, meaning no call conversion action will be performed.USE_ACCOUNT_LEVEL_CALL_CONVERSION_ACTION
, meaning the extension will use the call conversion type set at the account level.USE_RESOURCE_LEVEL_CALL_CONVERSION_ACTION
, meaning the extension will use the conversion action specified in theCallAsset.call_conversion_action
field. TheCallAsset.call_conversion_action
field will be ignored for any othercall_conversion_reporting_state
value.
When migrating a Feed-based Call extension, if its
call_conversion_tracking_disabled
is true
, then the new CallAsset'
s
call_conversion_reporting_state
should be set to DISABLED
. Note that the
default value for call_conversion_reporting_state
is
USE_RESOURCE_LEVEL_CALL_CONVERSION_ACTION
; you must explicitly set it to
DISABLED
to continue to disallow conversion tracking for this extension. This
change will be made as part of the automatic migration beginning April 8,
2022.
If you choose to migrate your own Feed-based Call extensions and are currently using conversion tracking with desktop Call ads, you should migrate all Call extension instances to Asset-based extensions in an account at the same time. This is to do with the assignment of Google forwarding numbers, which will only be given to Asset-based Call extensions once any are present in an account.
Tracking Asset-based extensions using ValueTrack parameters in URLs
All
ValueTrack parameters
will continue to function when used in URLs in Asset-based extensions except for
{feeditemid}
. Instead, use {extensionid}
to link any final URL, tracking
template, or custom parameter to an Asset-based extension. The {feeditemid}
parameter will no longer return any value once the Feed-based extension is
migrated to an Asset-based extension.
The automatic migration procedure will not change occurrences of
{feeditemid}
into {extensionid}
for you; you must update any URLs that
utilize the {feeditemid}
parameter to also include the {extensionid}
parameter. Doing so will ensure that you're tracking clicks to either type of
extension. Note, however, that only one of these fields will be populated when a
click occurs.
Serving of asset based extensions
Asset-based extensions are attached to serve in campaigns with links to various entities.
AdGroupAsset
- Extension will serve in this ad group.
CampaignAsset
- Extension will serve in this campaign and all of its ad groups.
CustomerAsset
- Extension will serve in any eligible campaign.
This is in contrast to the historical behavior:
AdGroupFeed
- Extension will serve in this ad group.
CampaignFeed
- Extension will serve in this campaign, except for ad groups with links to ad group feeds.
CustomerFeed
- Extension will serve in any eligible campaign or ad group, except campaigns and ad groups with extensions linked to campain feeds or ad group feeds, respectively.
In other words, lower-level attachment of assets would override higher levels in the past. However, targeting will be additive going forward.
Migration procedure
To migrate a Feed-based extension to an Asset-based extension, follow these steps:
- Identify an existing Feed-based extension that you intend to continue to use.
- Copy the Feed-based extension's contents into a new instance of the
corresponding Asset-based extension. For example, the contents of an
ExtensionFeedItem
of typePromotion
would be copied into a newPromotionAsset
object. - Associate the new Asset-based extension with the same ad groups and campaigns.
- Verify that the Asset-based extension is correctly configured.
- Remove the Feed-based extension from your account.
Once you've copied a Feed-based extension into an Asset-based extension, you should immediately discontinue any manipulation of this extension through the feeds-related services listed above.
You should remove the Feed-based extension after verifying that it has been accurately copied into an Asset-based extension.
An extension type is fully migrated when all feed items with a particular
PlaceholderType
have
been removed. See
removing extensions
for guidance.
The following example explains each step of the above process with a promotion extension.
Identify Feed-based extensions
To get a list of affected feed items, you'll need to run some GAQL queries on your accounts. We've provided GAQL queries to illustrate the general case below.
Native feeds
If your account has native feeds (those using Feed
,
FeedItem
, and
FeedMapping
), use the following queries
to retrieve the feed items that need to be migrated.
First retrieve all feeds that have a FeedMapping
for the extension type of interest.
SELECT
feed_mapping.feed,
feed_mapping.placeholder_type,
feed_mapping.attribute_field_mappings
FROM feed_mapping
WHERE feed_mapping.placeholder_type IN (
// Batch 1 extensions (auto-migrating October 2021).
'CALLOUT',
'PROMOTION',
'SITELINK',
'STRUCTURED_SNIPPET',
// Batch 2 extensions (auto-migrating April 2022).
'APP',
'CALL',
'PRICE'
)
Now you can use the feed_mapping.feed
to
retrieve the FeedItem
objects of interest.
SELECT feed_item.attribute_values
FROM feed_item
WHERE feed.resource_name = '<INSERT feed_mapping.feed>'
The feed_item.attribute_values
are
then interpreted according to the
feed_mapping.attribute_field_mappings
placeholders. These field mappings tell you how the extension fields are mapped
to the
feed_item.attribute_values
.
Extension setting services
When your extensions were created by the extension setting services
(CustomerExtensionSettingService
,
CampaignExtensionSettingService
,
AdGroupExtensionSettingService
,
etc.), it is easier to get the feed item IDs.
Use the following Google Ads Query Language query to request active extension feed item IDs of a particular extension type.
SELECT extension_feed_item.id
FROM extension_feed_item
WHERE extension_feed_item.status = 'ENABLED'
AND extension_feed_item.extension_type = 'PROMOTION'
The IDs returned can then be used to fetch extension details.
Fetch Feed-based extension details
Once you've identified a Feed-based extension to migrate, request its full details using its ID with another Google Ads Query Language query.
SELECT
extension_feed_item.id,
extension_feed_item.ad_schedules,
extension_feed_item.device,
extension_feed_item.status,
extension_feed_item.start_date_time,
extension_feed_item.end_date_time,
extension_feed_item.targeted_campaign,
extension_feed_item.targeted_ad_group,
extension_feed_item.promotion_feed_item.discount_modifier,
extension_feed_item.promotion_feed_item.final_mobile_urls,
extension_feed_item.promotion_feed_item.final_url_suffix,
extension_feed_item.promotion_feed_item.final_urls,
extension_feed_item.promotion_feed_item.language_code,
extension_feed_item.promotion_feed_item.money_amount_off.amount_micros,
extension_feed_item.promotion_feed_item.money_amount_off.currency_code,
extension_feed_item.promotion_feed_item.occasion,
extension_feed_item.promotion_feed_item.orders_over_amount.amount_micros,
extension_feed_item.promotion_feed_item.orders_over_amount.currency_code,
extension_feed_item.promotion_feed_item.percent_off,
extension_feed_item.promotion_feed_item.promotion_code,
extension_feed_item.promotion_feed_item.promotion_end_date,
extension_feed_item.promotion_feed_item.promotion_start_date,
extension_feed_item.promotion_feed_item.promotion_target,
extension_feed_item.promotion_feed_item.tracking_url_template
FROM extension_feed_item
WHERE extension_feed_item.extension_type = 'PROMOTION'
AND extension_feed_item.id = 123456789012
LIMIT 1
The returned ExtensionFeedItem
will have
all set values currently contained in the specified Feed-based extension that
can be applied to an Asset-based extension.
If using URL custom parameters, you will need to fetch them using an additional
Google Ads Query Language query. Add any results to the ExtensionFeedItem
instance retrieved by
the previous query. Note that the feed item ID in this query is the same as the
extension feed item ID used in the previous query.
SELECT feed_item.url_custom_parameters
FROM feed_item
WHERE feed_item.id = 123456789012
Create an Asset-based extension
The values fetched in the previous step can then be applied to a new instance of the relevant asset.
Java
private String createPromotionAssetFromFeed( GoogleAdsClient googleAdsClient, Long customerId, ExtensionFeedItem extensionFeedItem) { PromotionFeedItem promotionFeedItem = extensionFeedItem.getPromotionFeedItem(); // Creates the Promotion asset. Asset.Builder asset = Asset.newBuilder() .setName("Migrated from feed item " + extensionFeedItem.getId()) .setTrackingUrlTemplate(promotionFeedItem.getTrackingUrlTemplate()) .setFinalUrlSuffix(promotionFeedItem.getFinalUrlSuffix()) .setPromotionAsset( PromotionAsset.newBuilder() .setPromotionTarget(promotionFeedItem.getPromotionTarget()) .setDiscountModifier(promotionFeedItem.getDiscountModifier()) .setRedemptionEndDate(promotionFeedItem.getPromotionStartDate()) .setRedemptionEndDate(promotionFeedItem.getPromotionEndDate()) .setOccasion(promotionFeedItem.getOccasion()) .setLanguageCode(promotionFeedItem.getLanguageCode()) .addAllAdScheduleTargets(extensionFeedItem.getAdSchedulesList())) .addAllFinalUrls(promotionFeedItem.getFinalUrlsList()) .addAllFinalMobileUrls(promotionFeedItem.getFinalMobileUrlsList()) .addAllUrlCustomParameters(promotionFeedItem.getUrlCustomParametersList()); // Either PercentOff or MoneyAmountOff must be set. if (promotionFeedItem.getPercentOff() > 0) { // Adjusts the percent off scale when copying. asset.getPromotionAssetBuilder().setPercentOff(promotionFeedItem.getPercentOff() / 100); } else { asset.getPromotionAssetBuilder().setMoneyAmountOff(promotionFeedItem.getMoneyAmountOff()); } // Either PromotionCode or OrdersOverAmount must be set. if (promotionFeedItem.getPromotionCode() != null && promotionFeedItem.getPromotionCode().length() > 0) { asset.getPromotionAssetBuilder().setPromotionCode(promotionFeedItem.getPromotionCode()); } else { asset.getPromotionAssetBuilder().setOrdersOverAmount(promotionFeedItem.getOrdersOverAmount()); } // Sets the start and end dates if set in the existing extension. if (extensionFeedItem.hasStartDateTime()) { asset.getPromotionAssetBuilder().setStartDate(extensionFeedItem.getStartDateTime()); } if (extensionFeedItem.hasEndDateTime()) { asset.getPromotionAssetBuilder().setEndDate(extensionFeedItem.getEndDateTime()); } // Builds an operation to create the Promotion asset. AssetOperation operation = AssetOperation.newBuilder().setCreate(asset).build(); // Gets the Asset Service client. try (AssetServiceClient assetServiceClient = googleAdsClient.getLatestVersion().createAssetServiceClient()) { // Issues the request and returns the resource name of the new Promotion asset. MutateAssetsResponse response = assetServiceClient.mutateAssets(String.valueOf(customerId), ImmutableList.of(operation)); String resourceName = response.getResults(0).getResourceName(); System.out.println("Created Promotion asset with resource name " + resourceName); return resourceName; } }
C#
private string CreatePromotionAssetFromFeed(GoogleAdsClient client, long customerId, ExtensionFeedItem extensionFeedItem) { // Get the Asset Service client. AssetServiceClient assetServiceClient = client.GetService(Services.V13.AssetService); PromotionFeedItem promotionFeedItem = extensionFeedItem.PromotionFeedItem; // Create the Promotion asset. Asset asset = new Asset { // Name field is optional. Name = $"Migrated from feed item #{extensionFeedItem.Id}", PromotionAsset = new PromotionAsset { PromotionTarget = promotionFeedItem.PromotionTarget, DiscountModifier = promotionFeedItem.DiscountModifier, RedemptionStartDate = promotionFeedItem.PromotionStartDate, RedemptionEndDate = promotionFeedItem.PromotionEndDate, Occasion = promotionFeedItem.Occasion, LanguageCode = promotionFeedItem.LanguageCode, }, TrackingUrlTemplate = promotionFeedItem.TrackingUrlTemplate, FinalUrlSuffix = promotionFeedItem.FinalUrlSuffix }; // Either PercentOff or MoneyAmountOff must be set. if (promotionFeedItem.PercentOff > 0) { // Adjust the percent off scale when copying. asset.PromotionAsset.PercentOff = promotionFeedItem.PercentOff / 100; } else { asset.PromotionAsset.MoneyAmountOff = new Money { AmountMicros = promotionFeedItem.MoneyAmountOff.AmountMicros, CurrencyCode = promotionFeedItem.MoneyAmountOff.CurrencyCode }; } // Either PromotionCode or OrdersOverAmount must be set. if (!string.IsNullOrEmpty(promotionFeedItem.PromotionCode)) { asset.PromotionAsset.PromotionCode = promotionFeedItem.PromotionCode; } else { asset.PromotionAsset.OrdersOverAmount = new Money { AmountMicros = promotionFeedItem.OrdersOverAmount.AmountMicros, CurrencyCode = promotionFeedItem.OrdersOverAmount.CurrencyCode }; } // Set the start and end dates if set in the existing extension. if (extensionFeedItem.HasStartDateTime) { asset.PromotionAsset.StartDate = DateTime.Parse(extensionFeedItem.StartDateTime) .ToString("yyyy-MM-dd"); } if (extensionFeedItem.HasEndDateTime) { asset.PromotionAsset.EndDate = DateTime.Parse(extensionFeedItem.EndDateTime) .ToString("yyyy-MM-dd"); } asset.PromotionAsset.AdScheduleTargets.Add(extensionFeedItem.AdSchedules); asset.FinalUrls.Add(promotionFeedItem.FinalUrls); asset.FinalMobileUrls.Add(promotionFeedItem.FinalMobileUrls); asset.UrlCustomParameters.Add(promotionFeedItem.UrlCustomParameters); // Build an operation to create the Promotion asset. AssetOperation operation = new AssetOperation { Create = asset }; // Issue the request and return the resource name of the new Promotion asset. MutateAssetsResponse response = assetServiceClient.MutateAssets( customerId.ToString(), new[] { operation }); Console.WriteLine("Created Promotion asset with resource name " + $"{response.Results.First().ResourceName}"); return response.Results.First().ResourceName; }
PHP
private static function createPromotionAssetFromFeed( GoogleAdsClient $googleAdsClient, int $customerId, ExtensionFeedItem $extensionFeedItem ): string { $promotionFeedItem = $extensionFeedItem->getPromotionFeedItem(); // Creates the Promotion asset. $asset = new Asset([ // Name field is optional. 'name' => 'Migrated from feed item #' . $extensionFeedItem->getId(), 'promotion_asset' => new PromotionAsset([ 'promotion_target' => $promotionFeedItem->getPromotionTarget(), 'discount_modifier' => $promotionFeedItem->getDiscountModifier(), 'redemption_start_date' => $promotionFeedItem->getPromotionStartDate(), 'redemption_end_date' => $promotionFeedItem->getPromotionEndDate(), 'occasion' => $promotionFeedItem->getOccasion(), 'language_code' => $promotionFeedItem->getLanguageCode(), 'ad_schedule_targets' => $extensionFeedItem->getAdSchedules() ]), 'tracking_url_template' => $promotionFeedItem->getTrackingUrlTemplate(), 'url_custom_parameters' => $promotionFeedItem->getUrlCustomParameters(), 'final_urls' => $promotionFeedItem->getFinalUrls(), 'final_mobile_urls' => $promotionFeedItem->getFinalMobileUrls(), 'final_url_suffix' => $promotionFeedItem->getFinalUrlSuffix(), ]); // Either percent off or money amount off must be set. if ($promotionFeedItem->getPercentOff() > 0) { // Adjust the percent off scale when copying. $asset->getPromotionAsset()->setPercentOff($promotionFeedItem->getPercentOff() / 100); } else { $money = new Money([ 'amount_micros' => $promotionFeedItem->getMoneyAmountOff()->getAmountMicros(), 'currency_code' => $promotionFeedItem->getMoneyAmountOff()->getCurrencyCode() ]); $asset->getPromotionAsset()->setMoneyAmountOff($money); } // Either promotion code or orders over amount must be set. if (!empty($promotionFeedItem->getPromotionCode())) { $asset->getPromotionAsset()->setPromotionCode($promotionFeedItem->getPromotionCode()); } else { $money = new Money([ 'amount_micros' => $promotionFeedItem->getOrdersOverAmount()->getAmountMicros(), 'currency_code' => $promotionFeedItem->getOrdersOverAmount()->getCurrencyCode() ]); $asset->getPromotionAsset()->setOrdersOverAmount($money); } if ($extensionFeedItem->hasStartDateTime()) { $startDateTime = new \DateTime($extensionFeedItem->getStartDateTime()); $asset->getPromotionAsset()->setStartDate($startDateTime->format('yyyy-MM-dd')); } if ($extensionFeedItem->hasEndDateTime()) { $endDateTime = new \DateTime($extensionFeedItem->getEndDateTime()); $asset->getPromotionAsset()->setEndDate($endDateTime->format('yyyy-MM-dd')); } // Creates an operation to add the Promotion asset. $assetOperation = new AssetOperation(); $assetOperation->setCreate($asset); // Issues a mutate request to add the Promotion asset and prints its information. $assetServiceClient = $googleAdsClient->getAssetServiceClient(); $response = $assetServiceClient->mutateAssets($customerId, [$assetOperation]); $assetResourceName = $response->getResults()[0]->getResourceName(); printf( "Created the Promotion asset with resource name: '%s'.%s", $assetResourceName, PHP_EOL ); return $assetResourceName; }
Python
def create_promotion_asset_from_feed(client, customer_id, extension_feed_item): """Retrieves all campaigns associated with the given FeedItem resource name. Args: client: an initialized GoogleAdsClient instance. customer_id: a client customer ID. extension_feed_item: an extension feed item. Returns: the resource name of a newly created promotion asset. """ asset_service = client.get_service("AssetService") promotion_feed_item = extension_feed_item.promotion_feed_item # Create an asset operation to start building the new promotion asset using # data from the given extension feed item. asset_operation = client.get_type("AssetOperation") asset = asset_operation.create asset.name = f"Migrated from feed item ID '{extension_feed_item.id}'" asset.tracking_url_template = promotion_feed_item.tracking_url_template asset.final_url_suffix = promotion_feed_item.final_url_suffix asset.final_urls.extend(promotion_feed_item.final_urls) asset.final_mobile_urls.extend(promotion_feed_item.final_mobile_urls) promotion_asset = asset.promotion_asset promotion_asset.promotion_target = promotion_feed_item.promotion_target promotion_asset.discount_modifier = promotion_feed_item.discount_modifier promotion_asset.redemption_start_date = ( promotion_feed_item.promotion_start_date ) promotion_asset.redemption_end_date = promotion_feed_item.promotion_end_date promotion_asset.occasion = promotion_feed_item.occasion promotion_asset.language_code = promotion_feed_item.language_code promotion_asset.ad_schedule_targets.extend(extension_feed_item.ad_schedules) # Either percent_off or money_amount_off must be set. if promotion_feed_item.percent_off > 0: # Adjust the percent off scale after copying. Extension feed items # interpret 1,000,000 as 1% and assets interpret 1,000,000 as 100% so # to migrate the correct discount value we must divide it by 100. promotion_asset.percent_off = int(promotion_feed_item.percent_off / 100) else: # If percent_off is not set then copy money_amount_off. This field is # an instance of Money in both cases, so setting the field with # copy_from is possible. Using regular assignment is also valid here. client.copy_from( promotion_asset.money_amount_off, promotion_feed_item.money_amount_off, ) # Check if promotion_code field is set if promotion_feed_item.promotion_code: promotion_asset.promotion_code = promotion_feed_item.promotion_code else: # If promotion_code is not set then copy orders_over_amount. This field # is an instance of Money in both cases, so setting the field with # copy_from is possible. Using regular assignment is also valid here. client.copy_from( promotion_asset.orders_over_amount, promotion_feed_item.orders_over_amount, ) # Set the start and end dates if set in the existing extension. if promotion_feed_item.promotion_start_date: promotion_asset.start_date = promotion_feed_item.promotion_start_date if promotion_feed_item.promotion_end_date: promotion_asset.end_date = promotion_feed_item.promotion_end_date response = asset_service.mutate_assets( customer_id=customer_id, operations=[asset_operation] ) resource_name = response.results[0].resource_name print(f"Created promotion asset with resource name: '{resource_name}'") return resource_name
Ruby
def create_promotion_asset_from_feed(client, customer_id, extension_feed_item) # Create a Promotion asset that copies values from the specified extension feed item. asset_service = client.service.asset promotion_feed_item = extension_feed_item.promotion_feed_item # Create an asset operation to start building the new promotion asset using # data from the given extension feed item. asset_operation = client.operation.create_resource.asset do |asset| asset.name = "Migrated from feed item ID '#{extension_feed_item.id}'" asset.tracking_url_template = promotion_feed_item.tracking_url_template asset.final_url_suffix = promotion_feed_item.final_url_suffix asset.final_urls += promotion_feed_item.final_urls asset.final_mobile_urls += promotion_feed_item.final_mobile_urls # Create the Promotion asset. asset.promotion_asset = client.resource.promotion_asset do |pa| pa.promotion_target = promotion_feed_item.promotion_target pa.discount_modifier = promotion_feed_item.discount_modifier pa.redemption_start_date = promotion_feed_item.promotion_start_date pa.redemption_end_date = promotion_feed_item.promotion_end_date pa.occasion = promotion_feed_item.occasion pa.language_code = promotion_feed_item.language_code pa.ad_schedule_targets += extension_feed_item.ad_schedules # Either percent_off or money_amount_off must be set. if promotion_feed_item.percent_off.positive? # Adjust the percent off scale after copying. pa.percent_off = int(promotion_feed_item.percent_off / 100) else # If percent_off is not set then copy money_amount_off. This field is # an instance of Money in both cases, so setting the field with # copy_from is possible. Using regular assignment is also valid here. pa.money_amount_off = promotion_feed_item.money_amount_off end # Either promotion_code or orders_over_amount must be set. if promotion_feed_item.promotion_code.empty? pa.orders_over_amount = promotion_feed_item.orders_over_amount else pa.promotion_code = promotion_feed_item.promotion_code end # Set the start and end dates if set in the existing extension. unless promotion_feed_item.promotion_start_date.empty? pa.start_date = promotion_feed_item.promotion_start_date end unless promotion_feed_item.promotion_end_date.empty? pa.end_date = promotion_feed_item.promotion_end_date end end end response = asset_service.mutate_assets(customer_id: customer_id, operations: [asset_operation]) resource_name = response.results.first.resource_name puts "Created promotion asset with resource name: '#{resource_name}'" resource_name end
Perl
sub create_promotion_asset_from_feed { my ($api_client, $customer_id, $extension_feed_item) = @_; my $promotion_feed_item = $extension_feed_item->{promotionFeedItem}; # Create the Promotion asset. my $asset = Google::Ads::GoogleAds::V13::Resources::Asset->new({ name => "Migrated from feed item #" . $extension_feed_item->{id}, trackingUrlTemplate => $promotion_feed_item->{trackingUrlTemplate}, finalUrlSuffix => $promotion_feed_item->{finalUrlSuffix}, promotionAsset => Google::Ads::GoogleAds::V13::Common::PromotionAsset->new({ promotionTarget => $promotion_feed_item->{promotionTarget}, discountModifier => $promotion_feed_item->{discountModifier}, redemptionStartDate => $promotion_feed_item->{promotionStartDate}, redemptionEndDate => $promotion_feed_item->{promotionEndDate}, occasion => $promotion_feed_item->{occasion}, languageCode => $promotion_feed_item->{languageCode}})}); push @{$asset->{finalUrls}}, @{$promotion_feed_item->{finalUrls}}; # Copy optional fields if present in the existing extension. if (defined($extension_feed_item->{adSchedules})) { push @{$asset->{promotionAsset}{adScheduleTargets}}, @{$extension_feed_item->{adSchedules}}; } if (defined($promotion_feed_item->{finalMobileUrls})) { push @{$asset->{finalMobileUrls}}, @{$promotion_feed_item->{finalMobileUrls}}; } if (defined($promotion_feed_item->{urlCustomParameters})) { push @{$asset->{urlCustomParameters}}, @{$promotion_feed_item->{urlCustomParameters}}; } # Either percentOff or moneyAmountOff must be set. if (defined($promotion_feed_item->{percentOff})) { # Adjust the percent off scale when copying. $asset->{promotionAsset}{percentOff} = $promotion_feed_item->{percentOff} / 100; } else { $asset->{promotionAsset}{moneyAmountOff} = Google::Ads::GoogleAds::V13::Common::Money->new({ amountMicros => $promotion_feed_item->{moneyAmountOff}{amountMicros}, currencyCode => $promotion_feed_item->{moneyAmountOff}{currencyCode}}); } # Either promotionCode or ordersOverAmount must be set. if (defined($promotion_feed_item->{promotionCode})) { $asset->{promotionAsset}{promotionCode} = $promotion_feed_item->{promotionCode}; } else { $asset->{promotionAsset}{ordersOverAmount} = Google::Ads::GoogleAds::V13::Common::Money->new({ amountMicros => $promotion_feed_item->{ordersOverAmount}{amountMicros}, currencyCode => $promotion_feed_item->{ordersOverAmount}{currencyCode}} ); } # Set the start and end dates if set in the existing extension. if (defined($extension_feed_item->{startDateTime})) { $asset->{promotionAsset}{startDate} = substr($extension_feed_item->{startDateTime}, 0, index($extension_feed_item->{startDateTime}, ' ')); } if (defined($extension_feed_item->{endDateTime})) { $asset->{promotionAsset}{endDate} = substr($extension_feed_item->{endDateTime}, 0, index($extension_feed_item->{endDateTime}, ' ')); } # Build an operation to create the Promotion asset. my $operation = Google::Ads::GoogleAds::V13::Services::AssetService::AssetOperation->new({ create => $asset }); # Issue the request and return the resource name of the new Promotion asset. my $response = $api_client->AssetService()->mutate({ customerId => $customer_id, operations => [$operation]}); printf "Created Promotion asset with resource name '%s'.\n", $response->{results}[0]{resourceName}; return $response->{results}[0]{resourceName}; }
Associate the asset with campaigns and ad groups
The new asset can now be associated with the same customers, campaigns, and ad groups as the original Feed-based extension. First, fetch the resource IDs that are currently associated with the extension setting.
Java
private List<Long> getTargetedCampaignIds( GoogleAdsServiceClient client, Long customerId, String extensionFeedItemResourceName) { String query = "SELECT campaign.id, campaign_extension_setting.extension_feed_items " + "FROM campaign_extension_setting " + "WHERE campaign_extension_setting.extension_type = 'PROMOTION' " + " AND campaign.status != 'REMOVED'"; ServerStream<SearchGoogleAdsStreamResponse> serverStream = client .searchStreamCallable() .call( SearchGoogleAdsStreamRequest.newBuilder() .setCustomerId(String.valueOf(customerId)) .setQuery(query) .build()); List<Long> campaignIds = new ArrayList<>(); for (SearchGoogleAdsStreamResponse response : serverStream) { for (GoogleAdsRow row : response.getResultsList()) { Campaign campaign = row.getCampaign(); CampaignExtensionSetting extensionSetting = row.getCampaignExtensionSetting(); // Adds the campaign ID to the list of IDs if the extension feed item is // associated with this extension setting. if (extensionSetting.getExtensionFeedItemsList().contains(extensionFeedItemResourceName)) { campaignIds.add(campaign.getId()); System.out.println("Found matching campaign with ID " + campaign.getId()); } } } return campaignIds; }
C#
private List<long> GetTargetedCampaignIds(GoogleAdsServiceClient googleAdsServiceClient, long customerId, string extensionFeedResourceName) { List<long> campaignIds = new List<long>(); string query = @" SELECT campaign.id, campaign_extension_setting.extension_feed_items FROM campaign_extension_setting WHERE campaign_extension_setting.extension_type = 'PROMOTION' AND campaign.status != 'REMOVED'"; googleAdsServiceClient.SearchStream(customerId.ToString(), query, delegate (SearchGoogleAdsStreamResponse response) { foreach (GoogleAdsRow googleAdsRow in response.Results) { // Add the campaign ID to the list of IDs if the extension feed item is // associated with this extension setting. if (googleAdsRow.CampaignExtensionSetting.ExtensionFeedItems.Contains( extensionFeedResourceName)) { Console.WriteLine( $"Found matching campaign with ID {googleAdsRow.Campaign.Id}."); campaignIds.Add(googleAdsRow.Campaign.Id); } } } ); return campaignIds; }
PHP
private static function getTargetedCampaignIds( GoogleAdsClient $googleAdsClient, int $customerId, string $extensionFeedItemResourceName ): array { $campaignIds = []; $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); // Create a query that will retrieve the campaign extension settings. $query = "SELECT campaign.id, campaign_extension_setting.extension_feed_items " . "FROM campaign_extension_setting " . "WHERE campaign_extension_setting.extension_type = 'PROMOTION' " . "AND campaign.status != 'REMOVED'"; // Issue a search request to get the campaign extension settings. /** @var GoogleAdsServerStreamDecorator $stream */ $stream = $googleAdsServiceClient->searchStream($customerId, $query); foreach ($stream->iterateAllElements() as $googleAdsRow) { /** @var GoogleAdsRow $googleAdsRow */ // Add the campaign ID to the list of IDs if the extension feed item is // associated with this extension setting. if ( in_array( $extensionFeedItemResourceName, iterator_to_array( $googleAdsRow->getCampaignExtensionSetting()->getExtensionFeedItems() ) ) ) { printf( "Found matching campaign with ID %d.%s", $googleAdsRow->getCampaign()->getId(), PHP_EOL ); $campaignIds[] = $googleAdsRow->getCampaign()->getId(); } } return $campaignIds; }
Python
def get_targeted_campaign_ids(client, customer_id, resource_name): """Retrieves all campaigns associated with the given FeedItem resource name. Args: client: an initialized GoogleAdsClient instance. customer_id: a client customer ID. resource_name: an extension feed item resource name. Returns: a list of campaign IDs. """ ga_service = client.get_service("GoogleAdsService") query = """ SELECT campaign.id, campaign_extension_setting.extension_feed_items FROM campaign_extension_setting WHERE campaign_extension_setting.extension_type = 'PROMOTION' AND campaign.status != 'REMOVED'""" stream = ga_service.search_stream(customer_id=customer_id, query=query) campaign_ids = [] for batch in stream: for row in batch.results: feed_items = row.campaign_extension_setting.extension_feed_items if resource_name in feed_items: print(f"Found matching campaign with ID: '{row.campaign.id}'") campaign_ids.append(row.campaign.id) return campaign_ids
Ruby
def get_targeted_campaign_ids(client, customer_id, resource_name) # Finds and returns all of the campaigns that are associated with the specified # Promotion extension feed item. query = <<~QUERY SELECT campaign.id, campaign_extension_setting.extension_feed_items FROM campaign_extension_setting WHERE campaign_extension_setting.extension_type = 'PROMOTION' AND campaign.status != 'REMOVED' QUERY responses = client.service.google_ads.search_stream(customer_id: customer_id, query: query) campaign_ids = [] responses.each do |response| response.results.each do |row| feed_items = row.campaign_extension_setting.extension_feed_items if feed_items.include?(resource_name) puts "Found matching campaign with ID '#{row.campaign.id}'." campaign_ids << row.campaign.id end end end campaign_ids end
Perl
sub get_targeted_campaign_ids { my ($google_ads_service, $customer_id, $extension_feed_item_resource_name) = @_; my @campaign_ids; my $query = " SELECT campaign.id, campaign_extension_setting.extension_feed_items FROM campaign_extension_setting WHERE campaign_extension_setting.extension_type = 'PROMOTION' AND campaign.status != 'REMOVED'"; my $search_stream_request = Google::Ads::GoogleAds::V13::Services::GoogleAdsService::SearchGoogleAdsStreamRequest ->new({ customerId => $customer_id, query => $query }); my $search_stream_handler = Google::Ads::GoogleAds::Utils::SearchStreamHandler->new({ service => $google_ads_service, request => $search_stream_request }); $search_stream_handler->process_contents( sub { my $google_ads_row = shift; # Add the campaign ID to the list of IDs if the extension feed item # is associated with this extension setting. if (grep { $_ eq $extension_feed_item_resource_name } @{$google_ads_row->{campaignExtensionSetting}{extensionFeedItems}}) { printf "Found matching campaign with ID $google_ads_row->{campaign}{id}.\n"; push @campaign_ids, $google_ads_row->{campaign}{id}; } }); return @campaign_ids; }
Then, create and upload a new CampaignAsset
,
AdGroupAsset
, or
CustomerAsset
to link the asset with each
resource.
Java
private void associateAssetWithCampaigns( GoogleAdsClient googleAdsClient, Long customerId, String promotionAssetResourceName, List<Long> campaignIds) { if (campaignIds.isEmpty()) { System.out.println("Asset was not associated with any campaigns."); return; } // Constructs an operation to associate the asset with each campaign. List<CampaignAssetOperation> campaignAssetOperations = campaignIds.stream() .map( id -> CampaignAssetOperation.newBuilder() .setCreate( CampaignAsset.newBuilder() .setAsset(promotionAssetResourceName) .setFieldType(AssetFieldType.PROMOTION) .setCampaign(ResourceNames.campaign(customerId, id))) .build()) .collect(Collectors.toList()); // Creates a service client. try (CampaignAssetServiceClient campaignAssetServiceClient = googleAdsClient.getLatestVersion().createCampaignAssetServiceClient()) { // Issues the mutate request. MutateCampaignAssetsResponse response = campaignAssetServiceClient.mutateCampaignAssets( String.valueOf(customerId), campaignAssetOperations); // Prints some information about the result. for (MutateCampaignAssetResult result : response.getResultsList()) { System.out.println("Created campaign asset with resource name " + result.getResourceName()); } } }
C#
private void AssociateAssetWithCampaigns(GoogleAdsClient client, long customerId, string promotionAssetResourceName, List<long> campaignIds) { if (campaignIds.Count == 0) { Console.WriteLine("Asset was not associated with any campaigns."); return; } CampaignAssetServiceClient campaignAssetServiceClient = client.GetService(Services.V13 .CampaignAssetService); List<CampaignAssetOperation> operations = new List<CampaignAssetOperation>(); foreach (long campaignId in campaignIds) { operations.Add(new CampaignAssetOperation { Create = new CampaignAsset { Asset = promotionAssetResourceName, FieldType = AssetFieldTypeEnum.Types.AssetFieldType.Promotion, Campaign = ResourceNames.Campaign(customerId, campaignId), } }); } MutateCampaignAssetsResponse response = campaignAssetServiceClient.MutateCampaignAssets( customerId.ToString(), operations); foreach (MutateCampaignAssetResult result in response.Results) { Console.WriteLine($"Created campaign asset with resource name " + $"{result.ResourceName}."); } }
PHP
private static function associateAssetWithCampaigns( GoogleAdsClient $googleAdsClient, int $customerId, string $promotionAssetResourceName, array $campaignIds ) { if (empty($campaignIds)) { print 'Asset was not associated with any campaigns.' . PHP_EOL; return; } $operations = []; foreach ($campaignIds as $campaignId) { $operations[] = new CampaignAssetOperation([ 'create' => new CampaignAsset([ 'asset' => $promotionAssetResourceName, 'field_type' => AssetFieldType::PROMOTION, 'campaign' => ResourceNames::forCampaign($customerId, $campaignId) ]) ]); } // Issues a mutate request to add the campaign assets and prints their information. $campaignAssetServiceClient = $googleAdsClient->getCampaignAssetServiceClient(); $response = $campaignAssetServiceClient->mutateCampaignAssets($customerId, $operations); foreach ($response->getResults() as $addedCampaignAsset) { /** @var CampaignAsset $addedCampaignAsset */ printf( "Created campaign asset with resource name: '%s'.%s", $addedCampaignAsset->getResourceName(), PHP_EOL ); } }
Python
def associate_asset_with_campaigns( client, customer_id, promotion_asset_resource_name, campaign_ids ): """Associates the specified promotion asset with the specified campaigns. Args: client: an initialized GoogleAdsClient instance. customer_id: a client customer ID. promotion_asset_resource_name: the resource name for a promotion asset. campaign_ids: a list of campaign IDs. """ if len(campaign_ids) == 0: print(f"Asset was not associated with any campaigns.") return campaign_service = client.get_service("CampaignService") campaign_asset_service = client.get_service("CampaignAssetService") operations = [] for campaign_id in campaign_ids: operation = client.get_type("CampaignAssetOperation") campaign_asset = operation.create campaign_asset.asset = promotion_asset_resource_name campaign_asset.field_type = client.enums.AssetFieldTypeEnum.PROMOTION campaign_asset.campaign = campaign_service.campaign_path( customer_id, campaign_id ) operations.append(operation) response = campaign_asset_service.mutate_campaign_assets( customer_id=customer_id, operations=operations ) for result in response.results: print( "Created campaign asset with resource name: " f"'{result.resource_name}'" )
Ruby
def associate_asset_with_campaigns(client, customer_id, promotion_asset_resource_name, campaign_ids) # Associates the specified promotion asset with the specified campaigns. if campaign_ids.empty? puts 'Asset was not associated with any campaigns.' return end operations = campaign_ids.map do |campaign_id| client.operation.create_resource.campaign_asset do |ca| ca.asset = promotion_asset_resource_name ca.field_type = :PROMOTION ca.campaign = client.path.campaign(customer_id, campaign_id) end end response = client.service.campaign_asset.mutate_campaign_assets( customer_id: customer_id, operations: operations, ) response.results.each do |result| puts "Created campaign asset with resource name '#{result.resource_name}'." end end
Perl
sub associate_asset_with_campaigns { my ($api_client, $customer_id, $promotion_asset_resource_name, @campaign_ids) = @_; if (scalar(@campaign_ids) == 0) { printf "Asset was not associated with any campaigns.\n"; return (); } my $operations = []; foreach my $campaign_id (@campaign_ids) { my $campaign_asset = Google::Ads::GoogleAds::V13::Resources::CampaignAsset->new({ asset => $promotion_asset_resource_name, fieldType => PROMOTION, campaign => Google::Ads::GoogleAds::V13::Utils::ResourceNames::campaign( $customer_id, $campaign_id )}); my $operation = Google::Ads::GoogleAds::V13::Services::CampaignAssetService::CampaignAssetOperation ->new({ create => $campaign_asset }); push @$operations, $operation; } my $response = $api_client->CampaignAssetService()->mutate({ customerId => $customer_id, operations => $operations }); foreach my $result (@{$response->{results}}) { printf "Created campaign asset with resource name '%s'.\n", $result->{resourceName}; } }
Verify the asset contents
Use the following Google Ads Query Language query to fetch the contents of a promotion asset. Inspect the results to confirm that all relevant fields have been copied correctly.
At this point the two identical extensions are both active, but the Asset-based extension will serve instead of the Feed-based extension.
SELECT
asset.id,
asset.name,
asset.type,
asset.final_urls,
asset.final_mobile_urls,
asset.final_url_suffix,
asset.tracking_url_template,
asset.promotion_asset.promotion_target,
asset.promotion_asset.discount_modifier,
asset.promotion_asset.redemption_start_date,
asset.promotion_asset.redemption_end_date,
asset.promotion_asset.occasion,
asset.promotion_asset.language_code,
asset.promotion_asset.percent_off,
asset.promotion_asset.money_amount_off.amount_micros,
asset.promotion_asset.money_amount_off.currency_code,
asset.promotion_asset.promotion_code,
asset.promotion_asset.orders_over_amount.amount_micros,
asset.promotion_asset.orders_over_amount.currency_code,
asset.promotion_asset.start_date,
asset.promotion_asset.end_date,
asset.promotion_asset.ad_schedule_targets
FROM asset
WHERE asset.resource_name = 'customers/123456789/assets/123456789012'
Remove the Feed-based extension
Once you've verified that the Asset-based extension accurately reflects the original Feed-based extension, you should remove the Feed-based extension.
All existing Feed-based extensions will be automatically migrated on the auto migration dates. Remove Feed-based extensions once migrated to prevent multiple occurrences of the same extension.
Auto-migrated accounts detection
The API returns a
FeedErrorEnum.FeedError.legacy_extension_type_read_only
when attempting to modify extensions on an already migrated account. You can
detect such accounts by constructing a trivial mutate operation to one of the
feed services, for example, FeedItemService
).
Set the
validate_only
field on
the request to just check without applying the result. The response will
indicate if the account has been auto-migrated.
The migration is happening in phases, so it is possible for some extensions to be migrated and others not. See the guidance above in migration schedule for specific timings.
Feed reports availability
Historical feed reports will continue to be available until August 2022. Feed-based extensions will no longer generate new data after migration.
Asset contents can be requested by issuing a Google Ads Query Language query to the
asset
report. This report type is akin to the
extension_feed_item
report type for
Feed-based extensions.
An Asset-based extension's performance statistics can be requested by issuing a
Google Ads Query Language query to the asset_field_type_view
report. This report type is similar to the
feed_placeholder_view
report type for
Feed-based extensions.
Frequently asked questions (FAQ)
What happens to legacy extensions that are not fully supported in assets?
The asset will be created without the unsupported fields. For example, device preference is no longer available in assets, so extensions with device preference will be migrated to an asset without device preference.
What happens to legacy extensions that haven't passed ads policy review?
We will attempt to migrate extensions with a disapproved status; however, this might fail for some cases.
If the policy review was disapproved synchronously, then we won't migrate the asset. Most policy disapproval reasons fall into this bucket—for example, text too long, obscene language, and so on.
Otherwise, for more in-depth policy approvals that happen asynchronously, we will migrate the extension but it might become disapproved after migration.
What happens to reporting metrics for legacy extensions?
Metrics will be reset after migration to assets. The legacy metrics will be available for a short period of time after migration, but will be removed at some point in the future.
If you add one type of asset extension—for example, callout, are other types of extensions, such as price, still eligible to serve from feeds?
Yes. The link is only upgraded for the type of extensions that are linked. If you haven't linked a particular type of asset extension, feed-based extensions will continue to serve for that type.
Similarly, once you link a particular type of extension, feeds are no longer eligible to serve for that type of extension.
Note that you can have a mix of feeds and assets serving within the same campaign. If one ad group has an asset linked and another has a feed linked, both will be eligible to serve.
Will feed items be removed by the Google auto-migration?
No. We will remove the feed item link—for example, CampaignFeed
, but not
the original feed item.
Will Google auto-migration only migrate serving feed items?
No. We will migrate every feed item to asset, regardless of whether the feed
item is actively serving through a feed item link—for example,
CampaignFeed
.