자바
// Copyright 2022 Google LLC // // 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 // // https://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. package com.google.ads.googleads.examples.shoppingads; import com.beust.jcommander.Parameter; import com.google.ads.googleads.examples.utils.ArgumentNames; import com.google.ads.googleads.examples.utils.CodeSampleParams; import com.google.ads.googleads.lib.GoogleAdsClient; import com.google.ads.googleads.v17.enums.ListingGroupFilterListingSourceEnum.ListingGroupFilterListingSource; import com.google.ads.googleads.v17.enums.ListingGroupFilterProductConditionEnum.ListingGroupFilterProductCondition; import com.google.ads.googleads.v17.enums.ListingGroupFilterTypeEnum.ListingGroupFilterType; import com.google.ads.googleads.v17.errors.GoogleAdsError; import com.google.ads.googleads.v17.errors.GoogleAdsException; import com.google.ads.googleads.v17.resources.AssetGroupListingGroupFilter; import com.google.ads.googleads.v17.resources.ListingGroupFilterDimension; import com.google.ads.googleads.v17.resources.ListingGroupFilterDimension.ProductBrand; import com.google.ads.googleads.v17.resources.ListingGroupFilterDimension.ProductCondition; import com.google.ads.googleads.v17.services.AssetGroupListingGroupFilterOperation; import com.google.ads.googleads.v17.services.GoogleAdsRow; import com.google.ads.googleads.v17.services.GoogleAdsServiceClient; import com.google.ads.googleads.v17.services.GoogleAdsServiceClient.SearchPagedResponse; import com.google.ads.googleads.v17.services.MutateGoogleAdsRequest; import com.google.ads.googleads.v17.services.MutateGoogleAdsResponse; import com.google.ads.googleads.v17.services.MutateOperation; import com.google.ads.googleads.v17.services.MutateOperationResponse; import com.google.ads.googleads.v17.services.MutateOperationResponse.ResponseCase; import com.google.ads.googleads.v17.services.SearchGoogleAdsRequest; import com.google.ads.googleads.v17.utils.ResourceNames; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.LongStream; /** * This example shows how to add product partitions to a Performance Max retail campaign. * * <p>For Performance Max campaigns, product partitions are represented using the * AssetGroupListingGroupFilter resource. This resource can be combined with itself to form a * hierarchy that creates a product partition tree. * * <p>For more information about Performance Max retail campaigns, see the {@link * AddPerformanceMaxRetailCampaign} example. */ public class AddPerformanceMaxProductListingGroupTree { private final int TEMPORARY_ID_LISTING_GROUP_ROOT = -1; private static class AddPerformanceMaxProductListingGroupTreeParams extends CodeSampleParams { @Parameter(names = ArgumentNames.CUSTOMER_ID, required = true) private Long customerId; @Parameter(names = ArgumentNames.ASSET_GROUP_ID, required = true) private Long assetGroupId; @Parameter(names = ArgumentNames.REPLACE_EXISTING_TREE, required = true, arity = 1) private Boolean replaceExistingTree; } public static void main(String[] args) throws Exception { AddPerformanceMaxProductListingGroupTreeParams params = new AddPerformanceMaxProductListingGroupTreeParams(); if (!params.parseArguments(args)) { // Either pass the required parameters for this example on the command line, or insert them // into the code here. See the parameter class definition above for descriptions. params.customerId = Long.parseLong("INSERT_CUSTOMER_ID_HERE"); params.assetGroupId = Long.parseLong("INSERT_AD_GROUP_ID_HERE"); // Optional: To replace the existing listing group tree from the asset group set this // parameter to true. // If the current AssetGroup already has a tree of ListingGroupFilters, attempting to add a // new set of ListingGroupFilters including a root filter will result in an // 'ASSET_GROUP_LISTING_GROUP_FILTER_ERROR_MULTIPLE_ROOTS' error. Setting this option to true // will remove the existing tree and prevent this error. params.replaceExistingTree = Boolean.parseBoolean("INSERT_REPLACE_EXISTING_TREE_HERE"); } GoogleAdsClient googleAdsClient = null; try { googleAdsClient = GoogleAdsClient.newBuilder().fromPropertiesFile().build(); } catch (FileNotFoundException fnfe) { System.err.printf( "Failed to load GoogleAdsClient configuration from file. Exception: %s%n", fnfe); System.exit(1); } catch (IOException ioe) { System.err.printf("Failed to create GoogleAdsClient. Exception: %s%n", ioe); System.exit(1); } try { new AddPerformanceMaxProductListingGroupTree() .runExample( googleAdsClient, params.customerId, params.assetGroupId, params.replaceExistingTree); } catch (GoogleAdsException gae) { // GoogleAdsException is the base class for most exceptions thrown by an API request. // Instances of this exception have a message and a GoogleAdsFailure that contains a // collection of GoogleAdsErrors that indicate the underlying causes of the // GoogleAdsException. System.err.printf( "Request ID %s failed due to GoogleAdsException. Underlying errors:%n", gae.getRequestId()); int i = 0; for (GoogleAdsError googleAdsError : gae.getGoogleAdsFailure().getErrorsList()) { System.err.printf(" Error %d: %s%n", i++, googleAdsError); } System.exit(1); } } /** * A factory that creates MutateOperations for removing an existing tree of * AssetGroupListingGroupFilters. * * <p>AssetGroupListingGroupFilters must be removed in a specific order: all of the children of a * filter must be removed before the filter itself, otherwise the API will return an error. * * <p>This object is intended to be used with an array of MutateOperations to perform a series of * related updates to an AssetGroup. */ private static class AssetGroupListingGroupFilterRemoveOperationFactory { private String rootResourceName = ""; private final Map<String, Set<String>> parentsToChildren = new HashMap<>(); private AssetGroupListingGroupFilterRemoveOperationFactory( List<AssetGroupListingGroupFilter> resources) throws Exception { if (resources.isEmpty()) { throw new Exception("No listing group filters to remove"); } for (AssetGroupListingGroupFilter filter : resources) { if (filter.getParentListingGroupFilter().isEmpty() && !this.rootResourceName.isEmpty()) { // A node with no parent is the root node, but there can only be a single root node. throw new IllegalStateException("More than one root node"); } else if (filter.getParentListingGroupFilter().isEmpty()) { // Sets the root node. this.rootResourceName = filter.getResourceName(); } else { // Adds an entry to the parentsToChildren map for each non-root node. String parentResourceName = filter.getParentListingGroupFilter(); // Checks to see if a sibling in this group has already been visited, and fetches or // creates a new set as required. Set<String> siblings = this.parentsToChildren.computeIfAbsent(parentResourceName, p -> new HashSet<>()); siblings.add(filter.getResourceName()); } } } /** * Creates a list of MutateOperations that remove all of the resources in the tree originally * used to create this factory object. */ private List<MutateOperation> removeAll() { return removeDescendantsAndFilter(rootResourceName); } /** * Creates a list of MutateOperations that remove all the descendants of the specified * AssetGroupListingGroupFilter resource name. The order of removal is post-order, where all the * children (and their children, recursively) are removed first. Then, the node itself is * removed. */ private List<MutateOperation> removeDescendantsAndFilter(String resourceName) { List<MutateOperation> operations = new ArrayList<>(); if (this.parentsToChildren.containsKey(resourceName)) { Set<String> children = parentsToChildren.get(resourceName); for (String child : children) { // Recursively adds operations to the return value that remove each of the child nodes of // the current node from the tree. operations.addAll(removeDescendantsAndFilter(child)); } } // Creates and adds an operation to the return value that will remove the current node from // the tree. AssetGroupListingGroupFilterOperation operation = AssetGroupListingGroupFilterOperation.newBuilder().setRemove(resourceName).build(); operations.add( MutateOperation.newBuilder().setAssetGroupListingGroupFilterOperation(operation).build()); return operations; } } /** * A factory that creates MutateOperations wrapping AssetGroupListingGroupFilterMutateOperations * for a specific customerId and assetGroupId. * * <p>This object is intended to be used with an array of MutateOperations to perform an atomic * update to an AssetGroup. */ private static class AssetGroupListingGroupFilterCreateOperationFactory { private final long customerId; private final long assetGroupId; private final long rootListingGroupId; private static Iterator<Long> idGenerator; private AssetGroupListingGroupFilterCreateOperationFactory( long customerId, long assetGroupId, long rootListingGroupId) { this.customerId = customerId; this.assetGroupId = assetGroupId; this.rootListingGroupId = rootListingGroupId; // Generates a new temporary ID to be used for a resource name in a MutateOperation. See // https://developers.google.com/google-ads/api/docs/mutating/best-practices#temporary_resource_names // for details about temporary IDs. idGenerator = LongStream.iterate(rootListingGroupId - 1, prev -> prev - 1).iterator(); } private Long nextId() { return idGenerator.next(); } /** * Creates a MutateOperation that creates a root AssetGroupListingGroupFilter for the factory's * AssetGroup. * * <p>The root node or partition is the default, which is displayed as "All Products". */ private MutateOperation createRoot() { AssetGroupListingGroupFilter listingGroupFilter = AssetGroupListingGroupFilter.newBuilder() .setResourceName( ResourceNames.assetGroupListingGroupFilter( customerId, assetGroupId, rootListingGroupId)) .setAssetGroup(ResourceNames.assetGroup(customerId, assetGroupId)) // Since this is the root node, do not set the ParentListingGroupFilter. For all other // nodes, this would refer to the parent listing group filter resource name. // .setParentListingGroupFilter("PARENT_FILTER_NAME") // // Unlike AddPerformanceMaxRetailCampaign, the type for the root node here must be // SUBDIVISION because it will have child partitions under it. .setType(ListingGroupFilterType.SUBDIVISION) // Specifies that this uses the shopping listing source because it is a Performance // Max campaign for retail. .setListingSource(ListingGroupFilterListingSource.SHOPPING) // Note the case_value is not set because it should be undefined for the root node. .build(); AssetGroupListingGroupFilterOperation operation = AssetGroupListingGroupFilterOperation.newBuilder().setCreate(listingGroupFilter).build(); return MutateOperation.newBuilder() .setAssetGroupListingGroupFilterOperation(operation) .build(); } /** * Creates a MutateOperation that creates an intermediate AssetGroupListingGroupFilter for the * factory's AssetGroup. * * <p>Use this method if the filter will have child filters. Otherwise, use the {@link * #createUnit(long, long, ListingGroupFilterDimension), createUnit} method. * * @param parent the ID of the parent AssetGroupListingGroupFilter. * @param id the ID of AssetGroupListingGroupFilter that will be created. * @param dimension the dimension to associate with the AssetGroupListingGroupFilter. */ private MutateOperation createSubdivision( long parent, long id, ListingGroupFilterDimension dimension) { AssetGroupListingGroupFilter listingGroupFilter = AssetGroupListingGroupFilter.newBuilder() .setResourceName( ResourceNames.assetGroupListingGroupFilter(customerId, assetGroupId, id)) .setAssetGroup(ResourceNames.assetGroup(customerId, assetGroupId)) .setParentListingGroupFilter( ResourceNames.assetGroupListingGroupFilter(customerId, assetGroupId, parent)) // Uses the SUBDIVISION type to indicate that the AssetGroupListingGroupFilter // will have children. .setType(ListingGroupFilterType.SUBDIVISION) // Specifies that this uses the shopping listing source because it is a Performance // Max campaign for retail. .setListingSource(ListingGroupFilterListingSource.SHOPPING) .setCaseValue(dimension) .build(); AssetGroupListingGroupFilterOperation filterOperation = AssetGroupListingGroupFilterOperation.newBuilder().setCreate(listingGroupFilter).build(); return MutateOperation.newBuilder() .setAssetGroupListingGroupFilterOperation(filterOperation) .build(); } /** * Creates a MutateOperation that creates a child AssetGroupListingGroupFilter for the factory's * AssetGroup. * * <p>Use this method if the filter won't have child filters. Otherwise, use the {@link * #createSubdivision(long, long, ListingGroupFilterDimension), createSubdivision} method. * * @param parent the ID of the parent AssetGroupListingGroupFilter. * @param id the ID of AssetGroupListingGroupFilter that will be created. * @param dimension the dimension to associate with the AssetGroupListingGroupFilter. */ private MutateOperation createUnit( long parent, long id, ListingGroupFilterDimension dimension) { AssetGroupListingGroupFilter listingGroupFilter = AssetGroupListingGroupFilter.newBuilder() .setResourceName( ResourceNames.assetGroupListingGroupFilter(customerId, assetGroupId, id)) .setAssetGroup(ResourceNames.assetGroup(customerId, assetGroupId)) .setParentListingGroupFilter( ResourceNames.assetGroupListingGroupFilter(customerId, assetGroupId, parent)) // Uses the UNIT_INCLUDED type to indicate that the AssetGroupListingGroupFilter // won't have children. .setType(ListingGroupFilterType.UNIT_INCLUDED) // Specifies that this uses the shopping listing source because it is a Performance // Max campaign for retail. .setListingSource(ListingGroupFilterListingSource.SHOPPING) .setCaseValue(dimension) .build(); AssetGroupListingGroupFilterOperation filterOperation = AssetGroupListingGroupFilterOperation.newBuilder().setCreate(listingGroupFilter).build(); return MutateOperation.newBuilder() .setAssetGroupListingGroupFilterOperation(filterOperation) .build(); } } /** * Runs the example. * * @param googleAdsClient the Google Ads API client. * @param customerId the client customer ID. * @param assetGroupId the asset group id for the Performance Max campaign. * @param replaceExistingTree option to remove existing product tree from the passed in asset * group. * @throws GoogleAdsException if an API request failed with one or more service errors. */ private void runExample( GoogleAdsClient googleAdsClient, long customerId, long assetGroupId, boolean replaceExistingTree) throws Exception { String assetGroupResourceName = ResourceNames.assetGroup(customerId, assetGroupId); List<MutateOperation> operations = new ArrayList<>(); if (replaceExistingTree) { List<AssetGroupListingGroupFilter> existingListingGroupFilters = getAllExistingListingGroupFilterAssetsInAssetGroup( googleAdsClient, customerId, assetGroupResourceName); if (!existingListingGroupFilters.isEmpty()) { // A special factory object that ensures the creation of remove operations in the // correct order (child listing group filters must be removed before their parents). AssetGroupListingGroupFilterRemoveOperationFactory removeOperationFactory = new AssetGroupListingGroupFilterRemoveOperationFactory(existingListingGroupFilters); operations.addAll(removeOperationFactory.removeAll()); } } // Uses a factory to create all the MutateOperations that manipulate a specific // AssetGroup for a specific customer. The operations returned by the factory's methods // are used to construct a new tree of filters. These filters can have parent-child // relationships, and also include a special root that includes all children. // // When creating these filters, temporary IDs are used to create the hierarchy between // each of the nodes in the tree, beginning with the root listing group filter. // // The factory created below is specific to a customerId and assetGroupId. AssetGroupListingGroupFilterCreateOperationFactory createOperationFactory = new AssetGroupListingGroupFilterCreateOperationFactory( customerId, assetGroupId, TEMPORARY_ID_LISTING_GROUP_ROOT); // Creates the operation to add the root node of the tree. operations.add(createOperationFactory.createRoot()); // Creates an operation to add a leaf node for new products. ListingGroupFilterDimension newProductDimension = ListingGroupFilterDimension.newBuilder() .setProductCondition( ProductCondition.newBuilder() .setCondition(ListingGroupFilterProductCondition.NEW) .build()) .build(); operations.add( createOperationFactory.createUnit( TEMPORARY_ID_LISTING_GROUP_ROOT, createOperationFactory.nextId(), newProductDimension)); // Creates an operation to add a leaf node for used products. ListingGroupFilterDimension usedProductDimension = ListingGroupFilterDimension.newBuilder() .setProductCondition( ProductCondition.newBuilder() .setCondition(ListingGroupFilterProductCondition.USED) .build()) .build(); operations.add( createOperationFactory.createUnit( TEMPORARY_ID_LISTING_GROUP_ROOT, createOperationFactory.nextId(), usedProductDimension)); // This represents the ID of the "other" category in the ProductCondition subdivision. This ID // is saved because the node with this ID will be further partitioned, and this ID will serve as // the parent ID for subsequent child nodes of the "other" category. long otherSubdivisionId = createOperationFactory.nextId(); // Creates an operation to add a subdivision node for other products in the ProductCondition // subdivision. ListingGroupFilterDimension otherProductDimension = ListingGroupFilterDimension.newBuilder() .setProductCondition(ProductCondition.newBuilder().build()) .build(); operations.add( // Calls createSubdivision because this listing group will have children. createOperationFactory.createSubdivision( TEMPORARY_ID_LISTING_GROUP_ROOT, otherSubdivisionId, otherProductDimension)); // Creates an operation to add a leaf node for products with the brand "CoolBrand". ListingGroupFilterDimension coolBrandProductDimension = ListingGroupFilterDimension.newBuilder() .setProductBrand(ProductBrand.newBuilder().setValue("CoolBrand").build()) .build(); operations.add( createOperationFactory.createUnit( otherSubdivisionId, createOperationFactory.nextId(), coolBrandProductDimension)); // Creates an operation to add a leaf node for products with the brand "CheapBrand". ListingGroupFilterDimension cheapBrandProductDimension = ListingGroupFilterDimension.newBuilder() .setProductBrand(ProductBrand.newBuilder().setValue("CheapBrand").build()) .build(); operations.add( createOperationFactory.createUnit( otherSubdivisionId, createOperationFactory.nextId(), cheapBrandProductDimension)); // Creates an operation to add a leaf node for other products in the ProductBrand subdivision. ListingGroupFilterDimension otherBrandProductDimension = ListingGroupFilterDimension.newBuilder() .setProductBrand(ProductBrand.newBuilder().build()) .build(); operations.add( createOperationFactory.createUnit( otherSubdivisionId, createOperationFactory.nextId(), otherBrandProductDimension)); try (GoogleAdsServiceClient googleAdsServiceClient = googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) { MutateGoogleAdsRequest request = MutateGoogleAdsRequest.newBuilder() .setCustomerId(Long.toString(customerId)) .addAllMutateOperations(operations) .build(); MutateGoogleAdsResponse response = googleAdsServiceClient.mutate(request); printResponseDetails(request, response); } } /** * Fetches all of the listing group filters in an asset group. * * @param googleAdsClient the Google Ads API client. * @param customerId the client customer ID. * @param assetGroupResourceName the resource name of the asset group. */ private List<AssetGroupListingGroupFilter> getAllExistingListingGroupFilterAssetsInAssetGroup( GoogleAdsClient googleAdsClient, long customerId, String assetGroupResourceName) { String query = "SELECT " + "asset_group_listing_group_filter.resource_name, " + "asset_group_listing_group_filter.parent_listing_group_filter " + "FROM asset_group_listing_group_filter " + "WHERE " + "asset_group_listing_group_filter.asset_group = '" + assetGroupResourceName + "'"; SearchGoogleAdsRequest request = SearchGoogleAdsRequest.newBuilder() .setCustomerId(Long.toString(customerId)) .setQuery(query) .build(); List<AssetGroupListingGroupFilter> resources = new ArrayList<>(); try (GoogleAdsServiceClient googleAdsServiceClient = googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) { SearchPagedResponse searchPagedResponse = googleAdsServiceClient.search(request); for (GoogleAdsRow googleAdsRow : searchPagedResponse.iterateAll()) { resources.add(googleAdsRow.getAssetGroupListingGroupFilter()); } } return resources; } /** * Prints the details of a MutateGoogleAdsResponse. * * @param request a MutateGoogleAdsRequest instance. * @param response a MutateGoogleAdsResponse instance. */ private void printResponseDetails( MutateGoogleAdsRequest request, MutateGoogleAdsResponse response) { // Parse the Mutate response to print details about the entities that were removed and/or // created in the request. for (int i = 0; i < response.getMutateOperationResponsesCount(); i++) { MutateOperation operationRequest = request.getMutateOperations(i); MutateOperationResponse operationResponse = response.getMutateOperationResponses(i); if (operationResponse.getResponseCase() != ResponseCase.ASSET_GROUP_LISTING_GROUP_FILTER_RESULT) { String entityName = operationResponse.getResponseCase().toString(); // Trim the substring "_RESULT" from the end of the entity name. entityName = entityName.substring(0, entityName.lastIndexOf("_RESULT")); System.out.printf("Unsupported entity type: %s%n", entityName); } String resourceName = operationResponse.getAssetGroupListingGroupFilterResult().getResourceName(); AssetGroupListingGroupFilterOperation assetOperation = operationRequest.getAssetGroupListingGroupFilterOperation(); // Converts the type of operation (for example, "CREATE") to title case. String operationTypeString = assetOperation.getOperationCase().toString(); String operationTypeTitleCase = String.format( "%S%s", operationTypeString.substring(0, 1), operationTypeString.substring(1).toLowerCase()); // Prints information about the completed operation. System.out.printf( "%sd a(n) AssetGroupListingGroupFilter with resource name: '%s'%n", operationTypeTitleCase, resourceName); } } }
C#
// Copyright 2022 Google LLC // // 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. using CommandLine; using Google.Ads.Gax.Examples; using Google.Ads.GoogleAds.Lib; using Google.Ads.GoogleAds.V17.Errors; using Google.Ads.GoogleAds.V17.Resources; using Google.Ads.GoogleAds.V17.Services; using Google.Api.Gax; using System; using System.Collections.Generic; using System.Threading; using static Google.Ads.GoogleAds.V17.Enums.ListingGroupFilterListingSourceEnum.Types; using static Google.Ads.GoogleAds.V17.Enums.ListingGroupFilterProductConditionEnum.Types; using static Google.Ads.GoogleAds.V17.Enums.ListingGroupFilterTypeEnum.Types; using static Google.Ads.GoogleAds.V17.Resources.ListingGroupFilterDimension.Types; namespace Google.Ads.GoogleAds.Examples.V17 { /// <summary> /// This example shows how to add product partitions to a Performance Max retail campaign. /// /// For Performance Max campaigns, product partitions are represented using the /// AssetGroupListingGroupFilter resource. This resource can be combined with itself to form a /// hierarchy that creates a product partition tree. /// /// For more information about Performance Max retail campaigns, see the /// <see cref="AddPerformanceMaxRetailCampaign"/> example. /// </summary> public class AddPerformanceMaxProductListingGroupTree : ExampleBase { /// <summary> /// Command line options for running the <see /// cref="AddPerformanceMaxProductListingGroupTree"/> /// example. /// </summary> public class Options : OptionsBase { /// <summary> /// The Google Ads customer ID. /// </summary> [Option("customerId", Required = true, HelpText = "The Google Ads customer ID.")] public long CustomerId { get; set; } /// <summary> /// The Asset Group ID. /// </summary> [Option("assetGroupId", Required = true, HelpText = "The Asset Group ID.")] public long AssetGroupId { get; set; } /// <summary> /// An option to remove the listing group tree from the asset group when this example is /// run. /// </summary> [Option("replaceExistingTree", Required = false, HelpText = "An option that removes the existing listing group tree from the asset group.")] public bool ReplaceExistingTree { get; set; } } /// <summary> /// Main method, to run this code example as a standalone application. /// </summary> /// <param name="args">The command line arguments.</param> public static void Main(string[] args) { Options options = ExampleUtilities.ParseCommandLine<Options>(args); AddPerformanceMaxProductListingGroupTree codeExample = new AddPerformanceMaxProductListingGroupTree(); Console.WriteLine(codeExample.Description); codeExample.Run( new GoogleAdsClient(), options.CustomerId, options.AssetGroupId, options.ReplaceExistingTree ); } /// <summary> /// Returns a description about the code example. /// </summary> public override string Description => "This example shows how to add a product listing group tree to a " + "Performance Max retail campaign."; /// <summary> /// A factory that creates MutateOperations for removing an existing tree of /// AssetGroupListingGroupFilters. /// /// AssetGroupListingGroupFilters must be removed in a specific order: all of the children /// of a filter must be removed before the filter itself, otherwise the API will return an /// error. /// /// This object is intended to be used with an array of MutateOperations to perform a series /// of related updates to an AssetGroup. /// </summary> private class AssetGroupListingGroupFilterRemoveOperationFactory { private string rootResourceName; private Dictionary<string, AssetGroupListingGroupFilter> resources; private Dictionary<string, HashSet<string>> parentsToChildren; public AssetGroupListingGroupFilterRemoveOperationFactory( List<AssetGroupListingGroupFilter> resources) { if (resources.Count == 0) { throw new InvalidOperationException("No listing group filters to remove"); } this.resources = new Dictionary<string, AssetGroupListingGroupFilter>(); this.parentsToChildren = new Dictionary<string, HashSet<string>>(); foreach (AssetGroupListingGroupFilter filter in resources) { this.resources[filter.ResourceName] = filter; // When the node has no parent, it means it's the root node, which is treated // differently. if (string.IsNullOrEmpty(filter.ParentListingGroupFilter)) { if (!string.IsNullOrEmpty(this.rootResourceName)) { throw new InvalidOperationException("More than one root node"); } this.rootResourceName = filter.ResourceName; continue; } string parentResourceName = filter.ParentListingGroupFilter; HashSet<string> siblings; // Check to see if we've already visited a sibling in this group, and fetch or // create a new set as required. if (this.parentsToChildren.ContainsKey(parentResourceName)) { siblings = this.parentsToChildren[parentResourceName]; } else { siblings = new HashSet<string>(); } siblings.Add(filter.ResourceName); this.parentsToChildren[parentResourceName] = siblings; } } /// <summary> /// Creates a list of MutateOperations that remove all of the resources in the tree /// originally used to create this factory object. /// </summary> /// <returns>A list of MutateOperations</returns> public List<MutateOperation> RemoveAll() { return this.RemoveDescendentsAndFilter(this.rootResourceName); } /// <summary> /// Creates a list of MutateOperations that remove all the descendents of the specified /// AssetGroupListingGroupFilter resource name. The order of removal is post-order, /// where all the children (and their children, recursively) are removed first. Then, /// the node itself is removed. /// </summary> /// <returns>A list of MutateOperations</returns> public List<MutateOperation> RemoveDescendentsAndFilter(string resourceName) { List<MutateOperation> operations = new List<MutateOperation>(); if (this.parentsToChildren.ContainsKey(resourceName)) { HashSet<string> children = this.parentsToChildren[resourceName]; foreach (string child in children) { operations.AddRange(this.RemoveDescendentsAndFilter(child)); } } AssetGroupListingGroupFilterOperation operation = new AssetGroupListingGroupFilterOperation() { Remove = resourceName }; operations.Add( new MutateOperation() { AssetGroupListingGroupFilterOperation = operation } ); return operations; } } private const int TEMPORARY_ID_LISTING_GROUP_ROOT = -1; /// <summary> /// A factory that creates MutateOperations wrapping /// AssetGroupListingGroupFilterMutateOperations for a specific customerId and /// assetGroupId. /// /// This object is intended to be used with an array of MutateOperations to perform an /// atomic update to an AssetGroup. /// </summary> private class AssetGroupListingGroupFilterCreateOperationFactory { private long customerId; private long assetGroupId; private long rootListingGroupId; private long nextId; public AssetGroupListingGroupFilterCreateOperationFactory( long customerId, long assetGroupId, long rootListingGroupId) { this.customerId = customerId; this.assetGroupId = assetGroupId; this.rootListingGroupId = rootListingGroupId; this.nextId = this.rootListingGroupId - 1; } /// <summary> /// Returns a new temporary ID to be used for a resource name in a MutateOperation. See /// https://developers.google.com/google-ads/api/docs/mutating/best-practices#temporary_resource_names /// for details about temporary IDs. /// </summary> /// <returns>A new temporary ID.</returns> public long NextId() { long i = nextId; Interlocked.Decrement(ref nextId); return i; } /// <summary> /// Creates a MutateOperation that creates a root AssetGroupListingGroupFilter for the /// factory's AssetGroup. /// /// The root node or partition is the default, which is displayed as "All Products". /// </summary> /// <returns>A MutateOperation</returns> public MutateOperation CreateRoot() { AssetGroupListingGroupFilter listingGroupFilter = new AssetGroupListingGroupFilter() { ResourceName = ResourceNames.AssetGroupListingGroupFilter( this.customerId, this.assetGroupId, this.rootListingGroupId ), AssetGroup = ResourceNames.AssetGroup( this.customerId, this.assetGroupId ), // Since this is the root node, do not set the ParentListingGroupFilter. For all // other nodes, this would refer to the parent listing group filter resource // name. // ParentListingGroupFilter = "<PARENT FILTER NAME>" // Unlike AddPerformanceMaxRetailCampaign, the type for the root node here must // be Subdivision because we add child partitions under it. Type = ListingGroupFilterType.Subdivision, // Because this is a Performance Max campaign for retail, we need to specify // that this is in the shopping listing source. ListingSource = ListingGroupFilterListingSource.Shopping }; AssetGroupListingGroupFilterOperation operation = new AssetGroupListingGroupFilterOperation() { Create = listingGroupFilter }; return new MutateOperation() { AssetGroupListingGroupFilterOperation = operation }; } /// <summary> /// Creates a MutateOperation that creates a intermediate AssetGroupListingGroupFilter /// for the factory's AssetGroup. /// /// Use this method if the filter will have child filters. Otherwise, use the /// CreateUnit method. /// </summary> /// <param name="parent">The ID of the parent AssetGroupListingGroupFilter.</param> /// <param name="id">The ID of AssetGroupListingGroupFilter that will be /// created.</param> /// <param name="dimension">The dimension to associate with the /// AssetGroupListingGroupFilter.</param> /// <returns>A MutateOperation</returns> public MutateOperation CreateSubdivision( long parent, long id, ListingGroupFilterDimension dimension) { AssetGroupListingGroupFilter listingGroupFilter = new AssetGroupListingGroupFilter() { ResourceName = ResourceNames.AssetGroupListingGroupFilter( this.customerId, this.assetGroupId, id ), AssetGroup = ResourceNames.AssetGroup( this.customerId, this.assetGroupId ), ParentListingGroupFilter = ResourceNames.AssetGroupListingGroupFilter( this.customerId, this.assetGroupId, parent ), // We must use the Subdivision type to indicate that the // AssetGroupListingGroupFilter will have children. Type = ListingGroupFilterType.Subdivision, // Because this is a Performance Max campaign for retail, we need to specify // that this is in the shopping listing source. ListingSource = ListingGroupFilterListingSource.Shopping, CaseValue = dimension }; AssetGroupListingGroupFilterOperation filterOperation = new AssetGroupListingGroupFilterOperation() { Create = listingGroupFilter }; return new MutateOperation() { AssetGroupListingGroupFilterOperation = filterOperation }; } /// <summary> /// Creates a MutateOperation that creates a child AssetGroupListingGroupFilter /// for the factory's AssetGroup. /// /// Use this method if the filter won't have child filters. Otherwise, use the /// CreateSubdivision method. /// </summary> /// <param name="parent">The ID of the parent AssetGroupListingGroupFilter.</param> /// <param name="id">The ID of AssetGroupListingGroupFilter that will be /// created.</param> /// <param name="dimension">The dimension to associate with the /// AssetGroupListingGroupFilter.</param> /// <returns>A MutateOperation</returns> public MutateOperation CreateUnit( long parent, long id, ListingGroupFilterDimension dimension) { AssetGroupListingGroupFilter listingGroupFilter = new AssetGroupListingGroupFilter() { ResourceName = ResourceNames.AssetGroupListingGroupFilter( this.customerId, this.assetGroupId, id ), AssetGroup = ResourceNames.AssetGroup( this.customerId, this.assetGroupId ), ParentListingGroupFilter = ResourceNames.AssetGroupListingGroupFilter( this.customerId, this.assetGroupId, parent ), // We must use the UnitIncluded type to indicate that the // AssetGroupListingGroupFilter won't have children. Type = ListingGroupFilterType.UnitIncluded, // Because this is a Performance Max campaign for retail, we need to specify // that this is in the shopping listing source. ListingSource = ListingGroupFilterListingSource.Shopping, CaseValue = dimension }; AssetGroupListingGroupFilterOperation filterOperation = new AssetGroupListingGroupFilterOperation() { Create = listingGroupFilter }; return new MutateOperation() { AssetGroupListingGroupFilterOperation = filterOperation }; } } /// <summary> /// Runs the code example. /// </summary> /// <param name="client">The Google Ads client.</param> /// <param name="customerId">The Google Ads customer ID.</param> /// <param name="assetGroupId">The asset group id for the Performance Max campaign.</param> /// <param name="replaceExistingTree">Option to remove existing product tree /// from the passed in asset group.</param> public void Run( GoogleAdsClient client, long customerId, long assetGroupId, bool replaceExistingTree) { GoogleAdsServiceClient googleAdsServiceClient = client.GetService(Services.V17.GoogleAdsService); string assetGroupResourceName = ResourceNames.AssetGroup(customerId, assetGroupId); // We use a factory to create all the MutateOperations that manipulate a specific // AssetGroup for a specific customer. The operations returned by the factory's methods // are used to optionally remove all AssetGroupListingGroupFilters from the tree, and // then to construct a new tree of filters. These filters can have a parent-child // relationship, and also include a special root that includes all children. // // When creating these filters, we use temporary IDs to create the hierarchy between // the root listing group filter, and the subdivisions and leave nodes beneath that. // // The factory specific to a customerId and assetGroupId is created below. AssetGroupListingGroupFilterCreateOperationFactory createOperationFactory = new AssetGroupListingGroupFilterCreateOperationFactory( customerId, assetGroupId, TEMPORARY_ID_LISTING_GROUP_ROOT ); MutateGoogleAdsRequest request = new MutateGoogleAdsRequest { CustomerId = customerId.ToString() }; if (replaceExistingTree) { List<AssetGroupListingGroupFilter> existingListingGroupFilters = GetAllExistingListingGroupFilterAssetsInAssetGroup( client, customerId, assetGroupResourceName ); if (existingListingGroupFilters.Count > 0) { // A special factory object that ensures the creation of remove operations in the // correct order (child listing group filters must be removed before their parents). AssetGroupListingGroupFilterRemoveOperationFactory removeOperationFactory = new AssetGroupListingGroupFilterRemoveOperationFactory( existingListingGroupFilters ); request.MutateOperations.AddRange(removeOperationFactory.RemoveAll()); } } request.MutateOperations.Add(createOperationFactory.CreateRoot()); request.MutateOperations.Add( createOperationFactory.CreateUnit( TEMPORARY_ID_LISTING_GROUP_ROOT, createOperationFactory.NextId(), new ListingGroupFilterDimension() { ProductCondition = new ListingGroupFilterDimension.Types.ProductCondition() { Condition = ListingGroupFilterProductCondition.New } } ) ); request.MutateOperations.Add( createOperationFactory.CreateUnit( TEMPORARY_ID_LISTING_GROUP_ROOT, createOperationFactory.NextId(), new ListingGroupFilterDimension() { ProductCondition = new ListingGroupFilterDimension.Types.ProductCondition() { Condition = ListingGroupFilterProductCondition.Used } } ) ); // We save this ID because create child nodes underneath it. long subdivisionIdConditionOther = createOperationFactory.NextId(); request.MutateOperations.Add( // We're calling CreateSubdivision because this listing group will have children. createOperationFactory.CreateSubdivision( TEMPORARY_ID_LISTING_GROUP_ROOT, subdivisionIdConditionOther, new ListingGroupFilterDimension() { // All sibling nodes must have the same dimension type. We use an empty // ProductCondition to indicate that this is an "Other" partition. ProductCondition = new ListingGroupFilterDimension.Types.ProductCondition() } ) ); request.MutateOperations.Add( createOperationFactory.CreateUnit( subdivisionIdConditionOther, createOperationFactory.NextId(), new ListingGroupFilterDimension() { ProductBrand = new ProductBrand() { Value = "CoolBrand" } } ) ); request.MutateOperations.Add( createOperationFactory.CreateUnit( subdivisionIdConditionOther, createOperationFactory.NextId(), new ListingGroupFilterDimension() { ProductBrand = new ProductBrand() { Value = "CheapBrand" } } ) ); request.MutateOperations.Add( createOperationFactory.CreateUnit( subdivisionIdConditionOther, createOperationFactory.NextId(), new ListingGroupFilterDimension() { ProductBrand = new ProductBrand() } ) ); MutateGoogleAdsResponse response = googleAdsServiceClient.Mutate(request); PrintResponseDetails(request, response); } /// <summary> /// Fetches all of the listing group filters in an asset group. /// </summary> /// <param name="client">The Google Ads Client.</param> /// <param name="customerId">The Google Ads customer ID.</param> /// <param name="assetGroupResourceName">The resource name of the asset group.</param> /// <returns>A list of asset group listing filter resources.</returns> private List<AssetGroupListingGroupFilter> GetAllExistingListingGroupFilterAssetsInAssetGroup( GoogleAdsClient client, long customerId, string assetGroupResourceName) { List<AssetGroupListingGroupFilter> resources = new List<AssetGroupListingGroupFilter>(); // Get the GoogleAdsService. GoogleAdsServiceClient googleAdsService = client.GetService( Services.V17.GoogleAdsService); SearchGoogleAdsRequest request = new SearchGoogleAdsRequest() { CustomerId = customerId.ToString(), Query = $@" SELECT asset_group_listing_group_filter.resource_name, asset_group_listing_group_filter.parent_listing_group_filter FROM asset_group_listing_group_filter WHERE asset_group_listing_group_filter.asset_group = '{assetGroupResourceName}' " }; // The below enumerable will automatically iterate through the pages of the search // request. The limit to the number of listing group filters permitted in a Performance // Max campaign can be found here: // https://developers.google.com/google-ads/api/docs/best-practices/system-limits PagedEnumerable<SearchGoogleAdsResponse, GoogleAdsRow> searchPagedResponse = googleAdsService.Search(request); foreach (GoogleAdsRow row in searchPagedResponse) { resources.Add(row.AssetGroupListingGroupFilter); } return resources; } /// <summary> /// Prints the details of a MutateGoogleAdsResponse. Parses the "response" oneof field name /// and uses it to extract the new entity's name and resource name. /// </summary> /// <param name="request">A MutateGoogleAdsRequest instance.</param> /// <param name="response">A MutateGoogleAdsResponse instance.</param> private void PrintResponseDetails(MutateGoogleAdsRequest request, MutateGoogleAdsResponse response) { // Parse the Mutate response to print details about the entities that were created // in the request. for (int i = 0; i < response.MutateOperationResponses.Count; i++) { MutateOperation operationRequest = request.MutateOperations[i]; MutateOperationResponse operationResponse = response.MutateOperationResponses[i]; if (operationResponse.ResponseCase != MutateOperationResponse.ResponseOneofCase.AssetGroupListingGroupFilterResult) { string entityName = operationResponse.ResponseCase.ToString(); // Trim the substring "Result" from the end of the entity name. entityName = entityName.Remove(entityName.Length - 6); Console.WriteLine($"Unsupported entity type: {entityName}"); } string resourceName = operationResponse.AssetGroupListingGroupFilterResult.ResourceName; AssetGroupListingGroupFilterOperation assetOperation = operationRequest.AssetGroupListingGroupFilterOperation; switch (assetOperation.OperationCase) { case AssetGroupListingGroupFilterOperation.OperationOneofCase.Create: Console.WriteLine( $"Created a(n) AssetGroupListingGroupFilter with resource name: '{resourceName}'."); break; case AssetGroupListingGroupFilterOperation.OperationOneofCase.Remove: Console.WriteLine( $"Removed a(n) AssetGroupListingGroupFilter with resource name: '{resourceName}'."); break; default: Console.WriteLine($"Unsupported operation type: {assetOperation.OperationCase.ToString()}"); continue; } } } } }
PHP
<?php /** * Copyright 2022 Google LLC * * 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 * * https://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. */ namespace Google\Ads\GoogleAds\Examples\ShoppingAds; require __DIR__ . '/../../vendor/autoload.php'; use GetOpt\GetOpt; use Google\Ads\GoogleAds\Examples\Utils\ArgumentNames; use Google\Ads\GoogleAds\Examples\Utils\ArgumentParser; use Google\Ads\GoogleAds\Lib\OAuth2TokenBuilder; use Google\Ads\GoogleAds\Lib\V17\GoogleAdsClient; use Google\Ads\GoogleAds\Lib\V17\GoogleAdsClientBuilder; use Google\Ads\GoogleAds\Lib\V17\GoogleAdsException; use Google\Ads\GoogleAds\Util\V17\ResourceNames; use Google\Ads\GoogleAds\V17\Enums\ListingGroupFilterListingSourceEnum\ListingGroupFilterListingSource; use Google\Ads\GoogleAds\V17\Enums\ListingGroupFilterProductConditionEnum\ListingGroupFilterProductCondition; use Google\Ads\GoogleAds\V17\Enums\ListingGroupFilterTypeEnum\ListingGroupFilterType; use Google\Ads\GoogleAds\V17\Errors\GoogleAdsError; use Google\Ads\GoogleAds\V17\Resources\AssetGroupListingGroupFilter; use Google\Ads\GoogleAds\V17\Resources\ListingGroupFilterDimension; use Google\Ads\GoogleAds\V17\Resources\ListingGroupFilterDimension\ProductBrand; use Google\Ads\GoogleAds\V17\Resources\ListingGroupFilterDimension\ProductCondition; use Google\Ads\GoogleAds\V17\Services\AssetGroupListingGroupFilterOperation; use Google\Ads\GoogleAds\V17\Services\GoogleAdsRow; use Google\Ads\GoogleAds\V17\Services\MutateGoogleAdsRequest; use Google\Ads\GoogleAds\V17\Services\MutateGoogleAdsResponse; use Google\Ads\GoogleAds\V17\Services\MutateOperation; use Google\Ads\GoogleAds\V17\Services\MutateOperationResponse; use Google\Ads\GoogleAds\V17\Services\SearchGoogleAdsRequest; use Google\ApiCore\ApiException; use Google\ApiCore\Serializer; /** * This example shows how to add product partitions to a Performance Max retail campaign. * * For Performance Max campaigns, product partitions are represented using the * AssetGroupListingGroupFilter resource. This resource can be combined with itself to form a * hierarchy that creates a product partition tree. * * For more information about Performance Max retail campaigns, see the * AddPerformanceMaxRetailCampaign example. */ class AddPerformanceMaxProductListingGroupTree { private const CUSTOMER_ID = 'INSERT_CUSTOMER_ID_HERE'; private const ASSET_GROUP_ID = 'INSERT_ASSET_GROUP_ID_HERE'; // Optional: Removes the existing listing group tree from the asset group or not. // // If the current asset group already has a tree of listing group filters, and you // try to add a new set of listing group filters including a root filter, you'll // receive a 'ASSET_GROUP_LISTING_GROUP_FILTER_ERROR_MULTIPLE_ROOTS' error. // // Setting this option to true will remove the existing tree and prevent this error. private const REPLACE_EXISTING_TREE = false; // We specify temporary IDs that are specific to a single mutate request. // Temporary IDs are always negative and unique within one mutate request. private const LISTING_GROUP_ROOT_TEMPORARY_ID = -1; public static function main() { // Either pass the required parameters for this example on the command line, or insert them // into the constants above. $options = (new ArgumentParser())->parseCommandArguments([ ArgumentNames::CUSTOMER_ID => GetOpt::REQUIRED_ARGUMENT, ArgumentNames::ASSET_GROUP_ID => GetOpt::REQUIRED_ARGUMENT, ArgumentNames::REPLACE_EXISTING_TREE => GetOpt::OPTIONAL_ARGUMENT ]); // Generate a refreshable OAuth2 credential for authentication. $oAuth2Credential = (new OAuth2TokenBuilder())->fromFile()->build(); // Construct a Google Ads client configured from a properties file and the // OAuth2 credentials above. $googleAdsClient = (new GoogleAdsClientBuilder())->fromFile() ->withOAuth2Credential($oAuth2Credential) // We set this value to true to show how to use GAPIC v2 source code. You can remove the // below line if you wish to use the old-style source code. Note that in that case, you // probably need to modify some parts of the code below to make it work. // For more information, see // https://developers.devsite.corp.google.com/google-ads/api/docs/client-libs/php/gapic. ->usingGapicV2Source(true) ->build(); try { self::runExample( $googleAdsClient, $options[ArgumentNames::CUSTOMER_ID] ?: self::CUSTOMER_ID, $options[ArgumentNames::ASSET_GROUP_ID] ?: self::ASSET_GROUP_ID, filter_var( $options[ArgumentNames::REPLACE_EXISTING_TREE] ?: self::REPLACE_EXISTING_TREE, FILTER_VALIDATE_BOOLEAN ) ); } catch (GoogleAdsException $googleAdsException) { printf( "Request with ID '%s' has failed.%sGoogle Ads failure details:%s", $googleAdsException->getRequestId(), PHP_EOL, PHP_EOL ); foreach ($googleAdsException->getGoogleAdsFailure()->getErrors() as $error) { /** @var GoogleAdsError $error */ printf( "\t%s: %s%s", $error->getErrorCode()->getErrorCode(), $error->getMessage(), PHP_EOL ); } exit(1); } catch (ApiException $apiException) { printf( "ApiException was thrown with message '%s'.%s", $apiException->getMessage(), PHP_EOL ); exit(1); } } /** * Runs the example. * * @param GoogleAdsClient $googleAdsClient the Google Ads API client * @param int $customerId the customer ID * @param int $assetGroupId the asset group ID * @param bool $replaceExistingTree true if it should replace the existing listing group * tree on the asset group */ public static function runExample( GoogleAdsClient $googleAdsClient, int $customerId, int $assetGroupId, bool $replaceExistingTree ) { // We create all the mutate operations that manipulate a specific asset group for a specific // customer. The operations are used to optionally remove all asset group listing group // filters from the tree, and then to construct a new tree of filters. These filters can // have a parent-child relationship, and also include a special root that includes all // children. // // When creating these filters, we use temporary IDs to create the hierarchy between // the root listing group filter, and the subdivisions and leave nodes beneath that. $mutateOperations = []; if ($replaceExistingTree === true) { $existingListingGroupFilters = self::getAllExistingListingGroupFilterAssetsInAssetGroup( $googleAdsClient, $customerId, ResourceNames::forAssetGroup($customerId, $assetGroupId) ); if (count($existingListingGroupFilters) > 0) { $mutateOperations = array_merge( $mutateOperations, // Ensures the creation of remove operations in the correct order (child listing // group filters must be removed before their parents). self::createMutateOperationsForRemovingListingGroupFiltersTree( $existingListingGroupFilters ) ); } } $mutateOperations[] = self::createMutateOperationForRoot( $customerId, $assetGroupId, self::LISTING_GROUP_ROOT_TEMPORARY_ID ); // The temporary ID to be used for creating subdivisions and units. static $tempId = self::LISTING_GROUP_ROOT_TEMPORARY_ID - 1; $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, self::LISTING_GROUP_ROOT_TEMPORARY_ID, new ListingGroupFilterDimension([ 'product_condition' => new ProductCondition([ 'condition' => ListingGroupFilterProductCondition::PBNEW ]) ]) ); $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, self::LISTING_GROUP_ROOT_TEMPORARY_ID, new ListingGroupFilterDimension([ 'product_condition' => new ProductCondition([ 'condition' => ListingGroupFilterProductCondition::USED ]) ]) ); // We save this ID to create child nodes underneath it. $conditionOtherSubdivisionId = $tempId--; // We're calling createMutateOperationForSubdivision() because this listing group will // have children. $mutateOperations[] = self::createMutateOperationForSubdivision( $customerId, $assetGroupId, $conditionOtherSubdivisionId, self::LISTING_GROUP_ROOT_TEMPORARY_ID, new ListingGroupFilterDimension([ // All sibling nodes must have the same dimension type. We use an empty // ProductCondition to indicate that this is an "Other" partition. 'product_condition' => new ProductCondition() ]) ); $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, $conditionOtherSubdivisionId, new ListingGroupFilterDimension( ['product_brand' => new ProductBrand(['value' => 'CoolBrand'])] ) ); $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, $conditionOtherSubdivisionId, new ListingGroupFilterDimension([ 'product_brand' => new ProductBrand(['value' => 'CheapBrand']) ]) ); $mutateOperations[] = self::createMutateOperationForUnit( $customerId, $assetGroupId, $tempId--, $conditionOtherSubdivisionId, // All other product brands. new ListingGroupFilterDimension(['product_brand' => new ProductBrand()]) ); // Issues a mutate request to create everything and prints its information. $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); $response = $googleAdsServiceClient->mutate( MutateGoogleAdsRequest::build($customerId, $mutateOperations) ); self::printResponseDetails($mutateOperations, $response); } /** * Fetches all of the asset group listing group filters in an asset group. * * @param GoogleAdsClient $googleAdsClient the Google Ads API client * @param int $customerId the customer ID * @param string $assetGroupResourceName the resource name of the asset group * @return AssetGroupListingGroupFilter[] the list of asset group listing group filters */ private static function getAllExistingListingGroupFilterAssetsInAssetGroup( GoogleAdsClient $googleAdsClient, int $customerId, string $assetGroupResourceName ): array { $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); // Creates a query that retrieves asset group listing group filters. // The limit to the number of listing group filters permitted in a Performance // Max campaign can be found here: // https://developers.google.com/google-ads/api/docs/best-practices/system-limits. $query = sprintf( 'SELECT asset_group_listing_group_filter.resource_name, ' . 'asset_group_listing_group_filter.parent_listing_group_filter ' . 'FROM asset_group_listing_group_filter ' . 'WHERE asset_group_listing_group_filter.asset_group = "%s"', $assetGroupResourceName ); // Issues a search request. $response = $googleAdsServiceClient->search(SearchGoogleAdsRequest::build($customerId, $query)); $assetGroupListingGroupFilters = []; // Iterates over all rows in all pages to get an asset group listing group filter. foreach ($response->iterateAllElements() as $googleAdsRow) { /** @var GoogleAdsRow $googleAdsRow */ $assetGroupListingGroupFilters[] = $googleAdsRow->getAssetGroupListingGroupFilter(); } return $assetGroupListingGroupFilters; } /** * Creates mutate operations for removing an existing tree of asset group listing group filters. * * Asset group listing group filters must be removed in a specific order: all of the children * of a filter must be removed before the filter itself, otherwise the API will return an * error. * * @param AssetGroupListingGroupFilter[] $assetGroupListingGroupFilters the existing asset * group listing group filters * @return MutateOperation[] the list of MutateOperations to remove all listing groups */ private static function createMutateOperationsForRemovingListingGroupFiltersTree( array $assetGroupListingGroupFilters ): array { if (empty($assetGroupListingGroupFilters)) { throw new \UnexpectedValueException('No listing group filters to remove'); } $resourceNamesToListingGroupFilters = []; $parentsToChildren = []; $rootResourceName = null; foreach ($assetGroupListingGroupFilters as $assetGroupListingGroupFilter) { $resourceNamesToListingGroupFilters[$assetGroupListingGroupFilter->getResourceName()] = $assetGroupListingGroupFilter; // When the node has no parent, it means it's the root node, which is treated // differently. if (empty($assetGroupListingGroupFilter->getParentListingGroupFilter())) { if (!is_null($rootResourceName)) { throw new \UnexpectedValueException('More than one root node found.'); } $rootResourceName = $assetGroupListingGroupFilter->getResourceName(); continue; } $parentResourceName = $assetGroupListingGroupFilter->getParentListingGroupFilter(); $siblings = []; // Checks to see if we've already visited a sibling in this group and fetches it. if (array_key_exists($parentResourceName, $parentsToChildren)) { $siblings = $parentsToChildren[$parentResourceName]; } $siblings[] = $assetGroupListingGroupFilter->getResourceName(); $parentsToChildren[$parentResourceName] = $siblings; } return self::createMutateOperationsForRemovingDescendents( $rootResourceName, $parentsToChildren ); } /** * Creates a list of mutate operations that remove all the descendents of the specified * asset group listing group filter's resource name. The order of removal is post-order, * where all the children (and their children, recursively) are removed first. Then, * the node itself is removed. * * @param string $assetGroupListingGroupFilterResourceName the resource name of the root of * listing group tree * @param array $parentsToChildren the map from parent resource names to children resource * names * @return MutateOperation[] the list of MutateOperations to remove all listing groups */ private static function createMutateOperationsForRemovingDescendents( string $assetGroupListingGroupFilterResourceName, array $parentsToChildren ): array { $operations = []; if (array_key_exists($assetGroupListingGroupFilterResourceName, $parentsToChildren)) { foreach ($parentsToChildren[$assetGroupListingGroupFilterResourceName] as $child) { $operations = array_merge( $operations, self::createMutateOperationsForRemovingDescendents($child, $parentsToChildren) ); } } $operations[] = new MutateOperation([ 'asset_group_listing_group_filter_operation' => new AssetGroupListingGroupFilterOperation([ 'remove' => $assetGroupListingGroupFilterResourceName ]) ]); return $operations; } /** * Creates a mutate operation that creates a root asset group listing group filter for the * factory's asset group. * * The root node or partition is the default, which is displayed as "All Products". * * @param int $customerId the customer ID * @param int $assetGroupId the asset group ID * @param int $rootListingGroupId the root listing group ID * @return MutateOperation the mutate operation for creating the root */ private static function createMutateOperationForRoot( int $customerId, int $assetGroupId, int $rootListingGroupId ): MutateOperation { $assetGroupListingGroupFilter = new AssetGroupListingGroupFilter([ 'resource_name' => ResourceNames::forAssetGroupListingGroupFilter( $customerId, $assetGroupId, $rootListingGroupId ), 'asset_group' => ResourceNames::forAssetGroup($customerId, $assetGroupId), // Since this is the root node, do not set the 'parent_listing_group_filter' field. For // all other nodes, this would refer to the parent listing group filter resource // name. // Unlike AddPerformanceMaxRetailCampaign, the type for the root node here must // be SUBDIVISION because we add child partitions under it. 'type' => ListingGroupFilterType::SUBDIVISION, // Because this is a Performance Max campaign for retail, we need to specify // that this is in the shopping listing source. 'listing_source' => ListingGroupFilterListingSource::SHOPPING ]); return new MutateOperation([ 'asset_group_listing_group_filter_operation' => new AssetGroupListingGroupFilterOperation([ 'create' => $assetGroupListingGroupFilter ]) ]); } /** * Creates a mutate operation that creates a intermediate asset group listing group filter. * * @param int $customerId the customer ID * @param int $assetGroupId the asset group ID * @param int $assetGroupListingGroupFilterId the ID of the asset group listing group filter to * be created * @param int $parentId the ID of the parent of asset group listing group filter to be created * @param ListingGroupFilterDimension $listingGroupFilterDimension the listing group * filter dimension to associate with the asset group listing group filter * @return MutateOperation the mutate operation for creating a subdivision */ private static function createMutateOperationForSubdivision( int $customerId, int $assetGroupId, int $assetGroupListingGroupFilterId, int $parentId, ListingGroupFilterDimension $listingGroupFilterDimension ): MutateOperation { $assetGroupListingGroupFilter = new AssetGroupListingGroupFilter([ 'resource_name' => ResourceNames::forAssetGroupListingGroupFilter( $customerId, $assetGroupId, $assetGroupListingGroupFilterId ), 'asset_group' => ResourceNames::forAssetGroup($customerId, $assetGroupId), // Sets the type as a SUBDIVISION, which will allow the node to be the parent of // another sub-tree. 'type' => ListingGroupFilterType::SUBDIVISION, // Because this is a Performance Max campaign for retail, we need to specify // that this is in the shopping listing source. 'listing_source' => ListingGroupFilterListingSource::SHOPPING, 'parent_listing_group_filter' => ResourceNames::forAssetGroupListingGroupFilter( $customerId, $assetGroupId, $parentId ), // Case values contain the listing dimension used for the node. 'case_value' => $listingGroupFilterDimension ]); return new MutateOperation([ 'asset_group_listing_group_filter_operation' => new AssetGroupListingGroupFilterOperation([ 'create' => $assetGroupListingGroupFilter ]) ]); } /** * Creates a mutate operation that creates a child asset group listing group filter (unit * node). * * Use this method if the filter won't have child filters. Otherwise, use * createMutateOperationForSubdivision(). * * @param int $customerId the customer ID * @param int $assetGroupId the asset group ID * @param int $assetGroupListingGroupFilterId the ID of the asset group listing group filter to * be created * @param int $parentId the ID of the parent of asset group listing group filter to be * created * @param ListingGroupFilterDimension $listingGroupFilterDimension the listing group * filter dimension to associate with the asset group listing group filter * @return MutateOperation the mutate operation for creating a unit */ private static function createMutateOperationForUnit( int $customerId, int $assetGroupId, int $assetGroupListingGroupFilterId, string $parentId, ListingGroupFilterDimension $listingGroupFilterDimension ): MutateOperation { $assetGroupListingGroupFilter = new AssetGroupListingGroupFilter([ 'resource_name' => ResourceNames::forAssetGroupListingGroupFilter( $customerId, $assetGroupId, $assetGroupListingGroupFilterId ), 'asset_group' => ResourceNames::forAssetGroup($customerId, $assetGroupId), 'parent_listing_group_filter' => ResourceNames::forAssetGroupListingGroupFilter( $customerId, $assetGroupId, $parentId ), // Sets the type as a UNIT_INCLUDED to indicate that this asset group listing group // filter won't have children. 'type' => ListingGroupFilterType::UNIT_INCLUDED, // Because this is a Performance Max campaign for retail, we need to specify // that this is in the shopping listing source. 'listing_source' => ListingGroupFilterListingSource::SHOPPING, 'case_value' => $listingGroupFilterDimension ]); return new MutateOperation([ 'asset_group_listing_group_filter_operation' => new AssetGroupListingGroupFilterOperation([ 'create' => $assetGroupListingGroupFilter ]) ]); } /** * Prints the details of a mutate google ads response. Parses the "response" oneof field name * and uses it to extract the new entity's name and resource name. * * @param MutateOperation[] $mutateOperations the submitted mutate operations * @param MutateGoogleAdsResponse $mutateGoogleAdsResponse the mutate Google Ads response */ private static function printResponseDetails( array $mutateOperations, MutateGoogleAdsResponse $mutateGoogleAdsResponse ): void { foreach ( $mutateGoogleAdsResponse->getMutateOperationResponses() as $i => $operationResponse ) { /** @var MutateOperationResponse $operationResponse */ if ( $operationResponse->getResponse() !== 'asset_group_listing_group_filter_result' ) { // Trims the substring "_result" from the end of the entity name. printf( "Unsupported entity type: %s.%s", substr($operationResponse->getResponse(), 0, -strlen('_result')), PHP_EOL ); continue; } $operation = $mutateOperations[$i]->getAssetGroupListingGroupFilterOperation(); $getter = Serializer::getGetter($operationResponse->getResponse()); switch ($operation->getOperation()) { case 'create': printf( "Created an asset group listing group filter with resource name: " . " '%s'.%s", $operationResponse->$getter()->getResourceName(), PHP_EOL ); break; case 'remove': printf( "Removed an asset group listing group filter with resource name: " . " '%s'.%s", $operationResponse->$getter()->getResourceName(), PHP_EOL ); break; default: printf( "Unsupported operation type: '%s'.%s", $operation->getOperation(), PHP_EOL ); } } } } AddPerformanceMaxProductListingGroupTree::main();
Python
#!/usr/bin/env python # Copyright 2022 Google LLC # # 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 # # https://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. """Shows how to add product partitions to a Performance Max retail campaign. For Performance Max campaigns, product partitions are represented using the AssetGroupListingGroupFilter resource. This resource can be combined with itself to form a hierarchy that creates a product partition tree. For more information about Performance Max retail campaigns, see the shopping_ads/add_performance_max_retail_campaign.py example. """ import argparse import sys from google.ads.googleads.client import GoogleAdsClient from google.ads.googleads.errors import GoogleAdsException # We specify temporary IDs that are specific to a single mutate request. # Temporary IDs are always negative and unique within one mutate request. # # See https://developers.google.com/google-ads/api/docs/mutating/best-practices # for further details. # # These temporary IDs are fixed because they are used in multiple places. _TEMPORARY_ID_LISTING_GROUP_ROOT = -1 class AssetGroupListingGroupFilterRemoveOperationFactory: def __init__(self, client, listing_group_filters): """Factory class for creating sorted list of MutateOperations. The operations remove the given tree of AssetGroupListingGroupFilters, When removing these listing group filters, the remove operations must be sent in a specific order that removes leaf nodes before removing parent nodes. Args: client: an initialized GoogleAdsClient instance. listing_group_filters: a list of AssetGroupListingGroupFilters. """ if len(listing_group_filters) < 1: raise ValueError("No listing group filters to remove.") self.client = client self.root_resource_name = None self.parents_to_children = {} # Process the given list of listing group filters to identify the root # node and any parent to child edges in the tree. for listing_group_filter in listing_group_filters: resource_name = listing_group_filter.resource_name parent_resource_name = ( listing_group_filter.parent_listing_group_filter ) # When the node has no parent, it means it's the root node. if not parent_resource_name: if self.root_resource_name: # Check if another root node has already been detected and # raise an error if so, as only one root node can exist for # a given tree. raise ValueError("More than one listing group parent node.") else: self.root_resource_name = resource_name else: # Check if we've already visited a sibling in this group, and # either update it or create a new branch accordingly. if parent_resource_name in self.parents_to_children: # If we've visited a sibling already, add this resource # name to the existing list. self.parents_to_children[parent_resource_name].append( resource_name ) else: # If we haven't visited any siblings, then create a new list # for this parent node and add this resource name to it. self.parents_to_children[parent_resource_name] = [ resource_name ] def remove_all(self): """Creates a list of MutateOperations for the listing group filter tree. Returns: A list of MutateOperations that remove each specified AssetGroupListingGroupFilter in the tree passed in when this class was initialized. """ return self.remove_descendants_and_filter(self.root_resource_name) def remove_descendants_and_filter(self, resource_name): """Builds a post-order sorted list of MutateOperations. Creates a list of MutateOperations that remove all the descendents of the specified AssetGroupListingGroupFilter resource name. The order of removal is post-order, where all the children (and their children, recursively) are removed first. Then, the root node itself is removed. Args: resource_name: an AssetGroupListingGroupFilter resource name. Returns: a sorted list of MutateOperations. """ operations = [] # Check if resource name is a parent. if resource_name in self.parents_to_children: # If this resource name is a parent, call this method recursively # on each of its children. for child in self.parents_to_children[resource_name]: operations.extend(self.remove_descendants_and_filter(child)) mutate_operation = self.client.get_type("MutateOperation") mutate_operation.asset_group_listing_group_filter_operation.remove = ( resource_name ) operations.append(mutate_operation) return operations class AssetGroupListingGroupFilterCreateOperationFactory: def __init__(self, client, customer_id, asset_group_id, root_listing_id): """A factory class for creating MutateOperations. These operations create new AssetGroupListingGroupFilterMutateOperation instances using the given customer ID and asset group ID. Args: client: an initialized GoogleAdsClient instance. customer_id: a client customer ID. asset_group_id: the asset group id for the Performance Max campaign. root_listing_id: a temporary ID to use as the listing group root. """ self.client = client self.customer_id = customer_id self.asset_group_id = asset_group_id self.root_listing_id = root_listing_id self.next_temp_id = self.root_listing_id - 1 def next_id(self): """Returns the next temporary ID for use in a sequence. The temporary IDs are used in the list of MutateOperations in order to refer to objects in the request that aren't in the API yet. For more details see: https://developers.google.com/google-ads/api/docs/mutating/best-practices#temporary_resource_names Returns: A new temporary ID. """ self.next_temp_id -= 1 return self.next_temp_id def create_root(self): """Creates a MutateOperation to add a root AssetGroupListingGroupFilter. Returns: A MutateOperation for a new AssetGroupListingGroupFilter. """ googleads_service = self.client.get_service("GoogleAdsService") mutate_operation = self.client.get_type("MutateOperation") asset_group_listing_group_filter = ( mutate_operation.asset_group_listing_group_filter_operation.create ) asset_group_listing_group_filter.resource_name = ( googleads_service.asset_group_listing_group_filter_path( self.customer_id, self.asset_group_id, self.root_listing_id ) ) asset_group_listing_group_filter.asset_group = ( googleads_service.asset_group_path( self.customer_id, self.asset_group_id ) ) # Since this is the root node, do not set the # parent_listing_group_filter field. For all other nodes, this would # refer to the parent listing group filter resource name. # asset_group_listing_group_filter.parent_listing_group_filter = "<PARENT FILTER NAME>" # Unlike the add_performance_max_retail_campaign example, the type for # the root node here must be a subdivision because we add child # partitions under it. asset_group_listing_group_filter.type_ = ( self.client.enums.ListingGroupFilterTypeEnum.SUBDIVISION ) # Because this is a Performance Max campaign for retail, we need to # specify that this is a shopping listing source. asset_group_listing_group_filter.listing_source = ( self.client.enums.ListingGroupFilterListingSourceEnum.SHOPPING ) return mutate_operation def create_subdivision(self, parent_id, temporary_id, dimension): """Creates a MutateOperation to add an AssetGroupListingGroupFilter. Use this method if the filter will have child filters. Otherwise use the create_unit method. Args: parent_id: the ID of the parent AssetGroupListingGroupFilter. temporary_id: a temporary ID for the operation being created. dimension: The dimension to associate with this new AssetGroupListingGroupFilter. Returns: a MutateOperation for a new AssetGroupListingGroupFilter """ googleads_service = self.client.get_service("GoogleAdsService") mutate_operation = self.client.get_type("MutateOperation") asset_group_listing_group_filter = ( mutate_operation.asset_group_listing_group_filter_operation.create ) asset_group_listing_group_filter.resource_name = ( googleads_service.asset_group_listing_group_filter_path( self.customer_id, self.asset_group_id, temporary_id ) ) asset_group_listing_group_filter.asset_group = ( googleads_service.asset_group_path( self.customer_id, self.asset_group_id ) ) asset_group_listing_group_filter.parent_listing_group_filter = ( googleads_service.asset_group_listing_group_filter_path( self.customer_id, self.asset_group_id, parent_id ) ) # We must use the Subdivision type to indicate that the # AssetGroupListingGroupFilter will have children. asset_group_listing_group_filter.type_ = ( self.client.enums.ListingGroupFilterTypeEnum.SUBDIVISION ) # Because this is a Performance Max campaign for retail, we need to # specify that this is in the shopping listing source. asset_group_listing_group_filter.listing_source = ( self.client.enums.ListingGroupFilterListingSourceEnum.SHOPPING ) asset_group_listing_group_filter.case_value = dimension return mutate_operation def create_unit(self, parent_id, temporary_id, dimension): """Creates a MutateOperation to add an AssetGroupListingGroupFilter. Use this method if the filter will not have child filters. Otherwise use the create_subdivision method. Args: parent_id: the ID of the parent AssetGroupListingGroupFilter. dimension: The dimension to associate with this new AssetGroupListingGroupFilter. Returns: a MutateOperation for a new AssetGroupListingGroupFilter """ googleads_service = self.client.get_service("GoogleAdsService") mutate_operation = self.client.get_type("MutateOperation") asset_group_listing_group_filter = ( mutate_operation.asset_group_listing_group_filter_operation.create ) asset_group_listing_group_filter.resource_name = ( googleads_service.asset_group_listing_group_filter_path( self.customer_id, self.asset_group_id, temporary_id ) ) asset_group_listing_group_filter.asset_group = ( googleads_service.asset_group_path( self.customer_id, self.asset_group_id ) ) asset_group_listing_group_filter.parent_listing_group_filter = ( googleads_service.asset_group_listing_group_filter_path( self.customer_id, self.asset_group_id, parent_id ) ) # We must use the UnitIncluded type to indicate that the # AssetGroupListingGroupFilter won't have children. asset_group_listing_group_filter.type_ = ( self.client.enums.ListingGroupFilterTypeEnum.UNIT_INCLUDED ) # Because this is a Performance Max campaign for retail, we need to # specify that this is in the shopping listing source. asset_group_listing_group_filter.listing_source = ( self.client.enums.ListingGroupFilterListingSourceEnum.SHOPPING ) asset_group_listing_group_filter.case_value = dimension return mutate_operation def main(client, customer_id, asset_group_id, replace_existing_tree): """The main method that creates all necessary entities for the example. Args: client: an initialized GoogleAdsClient instance. customer_id: a client customer ID. asset_group_id: the asset group id for the Performance Max campaign. replace_existing_tree: option to remove existing product tree from the passed in asset group. """ googleads_service = client.get_service("GoogleAdsService") asset_group_resource_name = googleads_service.asset_group_path( customer_id, asset_group_id ) operations = [] if replace_existing_tree: # Retrieve a list of existing AssetGroupListingGroupFilters existing_listing_group_filters = ( get_all_existing_listing_group_filter_assets_in_asset_group( client, customer_id, asset_group_resource_name ) ) # If present, create MutateOperations to remove each # AssetGroupListingGroupFilter and add them to the list of operations. if existing_listing_group_filters: remove_operation_factory = ( AssetGroupListingGroupFilterRemoveOperationFactory( client, existing_listing_group_filters ) ) operations.extend(remove_operation_factory.remove_all()) create_operation_factory = ( AssetGroupListingGroupFilterCreateOperationFactory( client, customer_id, asset_group_id, _TEMPORARY_ID_LISTING_GROUP_ROOT, ) ) operations.append(create_operation_factory.create_root()) new_dimension = client.get_type("ListingGroupFilterDimension") new_dimension.product_condition.condition = ( client.enums.ListingGroupFilterProductConditionEnum.NEW ) operations.append( create_operation_factory.create_unit( _TEMPORARY_ID_LISTING_GROUP_ROOT, create_operation_factory.next_id(), new_dimension, ) ) used_dimension = client.get_type("ListingGroupFilterDimension") used_dimension.product_condition.condition = ( client.enums.ListingGroupFilterProductConditionEnum.USED ) operations.append( create_operation_factory.create_unit( _TEMPORARY_ID_LISTING_GROUP_ROOT, create_operation_factory.next_id(), used_dimension, ) ) # We save this ID because create child nodes underneath it. subdivision_id_condition_other = create_operation_factory.next_id() # All sibling nodes must have the same dimension type. We use an empty # product_condition to indicate that this is an "Other" partition. other_dimension = client.get_type("ListingGroupFilterDimension") # This triggers the presence of the product_condition field without # specifying any field values. This is important in order to tell the API # that this is an "other" node. other_dimension.product_condition._pb.SetInParent() # We're calling create_subdivision because this listing group will have # children. operations.append( create_operation_factory.create_subdivision( _TEMPORARY_ID_LISTING_GROUP_ROOT, subdivision_id_condition_other, other_dimension, ) ) cool_dimension = client.get_type("ListingGroupFilterDimension") cool_dimension.product_brand.value = "CoolBrand" operations.append( create_operation_factory.create_unit( subdivision_id_condition_other, create_operation_factory.next_id(), cool_dimension, ) ) cheap_dimension = client.get_type("ListingGroupFilterDimension") cheap_dimension.product_brand.value = "CheapBrand" operations.append( create_operation_factory.create_unit( subdivision_id_condition_other, create_operation_factory.next_id(), cheap_dimension, ) ) empty_dimension = client.get_type("ListingGroupFilterDimension") # This triggers the presence of the product_brand field without specifying # any field values. This is important in order to tell the API # that this is an "other" node. empty_dimension.product_brand._pb.SetInParent() operations.append( create_operation_factory.create_unit( subdivision_id_condition_other, create_operation_factory.next_id(), empty_dimension, ) ) response = googleads_service.mutate( customer_id=customer_id, mutate_operations=operations ) print_response_details(operations, response) def get_all_existing_listing_group_filter_assets_in_asset_group( client, customer_id, asset_group_resource_name ): """Fetches all of the listing group filters in an asset group. Args: client: an initialized GoogleAdsClient instance. customer_id: a client customer ID. asset_group_resource_name: the asset group resource name for the Performance Max campaign. Returns: a list of AssetGroupListingGroupFilters. """ query = f""" SELECT asset_group_listing_group_filter.resource_name, asset_group_listing_group_filter.parent_listing_group_filter FROM asset_group_listing_group_filter WHERE asset_group_listing_group_filter.asset_group = '{asset_group_resource_name}'""" request = client.get_type("SearchGoogleAdsRequest") request.customer_id = customer_id request.query = query googleads_service = client.get_service("GoogleAdsService") response = googleads_service.search(request=request) return [result.asset_group_listing_group_filter for result in response] def print_response_details(mutate_operations, response): """Prints the details of the GoogleAdsService.Mutate request. This uses the original list of mutate operations to map the operation result to what was sent. It can be assumed that the initial set of operations and the list returned in the response are in the same order. Args: mutate_operations: a list of MutateOperation instances. response: a GoogleAdsMutateResponse instance. """ # Parse the Mutate response to print details about the entities that were # created in the request. for i, result in enumerate(response.mutate_operation_responses): requested = mutate_operations[i] resource_name = ( result.asset_group_listing_group_filter_result.resource_name ) # Check the operation type for the requested operation in order to # log whether it was a remove or a create request. if "remove" in requested.asset_group_listing_group_filter_operation: print( "Removed an AssetGroupListingGroupFilter with resource name: " f"'{resource_name}'." ) elif "create" in requested.asset_group_listing_group_filter_operation: print( "Created an AssetGroupListingGroupFilter with resource name: " f"'{resource_name}'." ) else: print("An unknown operation was returned.") if __name__ == "__main__": parser = argparse.ArgumentParser( description=( "Adds product partitions to a Performance Max retail campaign." ) ) # The following argument(s) should be provided to run the example. parser.add_argument( "-c", "--customer_id", type=str, required=True, help="The Google Ads customer ID.", ) parser.add_argument( "-a", "--asset_group_id", type=int, required=True, help="The asset group id for the Performance Max campaign.", ) parser.add_argument( "-r", "--replace_existing_tree", action="store_true", help=( "Whether or not to replace the existing product partition tree. " "If the current AssetGroup already has a tree of " "ListingGroupFilters, attempting to add a new set of " "ListingGroupFilters including a root filter will result in an " "ASSET_GROUP_LISTING_GROUP_FILTER_ERROR_MULTIPLE_ROOTS error. " "Setting this option to true will remove the existing tree and " "prevent this error." ), ) args = parser.parse_args() # GoogleAdsClient will read the google-ads.yaml configuration file in the # home directory if none is specified. googleads_client = GoogleAdsClient.load_from_storage(version="v17") try: main( googleads_client, args.customer_id, args.asset_group_id, args.replace_existing_tree, ) except GoogleAdsException as ex: print( f'Request with ID "{ex.request_id}" failed with status ' f'"{ex.error.code().name}" and includes the following errors:' ) for error in ex.failure.errors: print(f'Error with message "{error.message}".') if error.location: for field_path_element in error.location.field_path_elements: print(f"\t\tOn field: {field_path_element.field_name}") sys.exit(1)
Ruby
#!/usr/bin/env ruby # Encoding: utf-8 # # Copyright 2022 Google LLC # # 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 # # https://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 shows how to add product partitions to a Performance Max retail # campaign. # # For Performance Max campaigns, product partitions are represented using the # AssetGroupListingGroupFilter resource. This resource can be combined with # itself to form a hierarchy that creates a product partition tree. # # For more information about Performance Max retail campaigns, see the # add_performance_max_retail_campaign.rb example. require 'optparse' require 'google/ads/google_ads' # We specify temporary IDs that are specific to a single mutate request. # Temporary IDs are always negative and unique within one mutate request. # # See https://developers.google.com/google-ads/api/docs/mutating/best-practices # for further details. TEMPORARY_ID_LISTING_GROUP_ROOT = "-1" def add_performance_max_product_listing_group_tree( customer_id, asset_group_id, replace_existing_tree) # GoogleAdsClient will read a config file from # ENV['HOME']/google_ads_config.rb when called without parameters client = Google::Ads::GoogleAds::GoogleAdsClient.new asset_group_resource_name = client.path.asset_group( customer_id, asset_group_id, ) # We use a factory to create all the MutateOperations that manipulate a # specific AssetGroup for a specific customer. The operations returned by the # factory's methods are used to optionally remove all # AssetGroupListingGroupFilters from the tree, and then to construct a new # tree of filters. These filters can have a parent-child relationship, and # also include a special root that includes all children. # # When creating these filters, we use temporary IDs to create the hierarchy # between the root listing group filter, and the subdivisions and leave nodes # beneath that. # # The factory specific to a customerId and assetGroupId is created below. create_operation_factory = AssetGroupListingGroupFilterCreateOperationFactory.new( customer_id, asset_group_id, TEMPORARY_ID_LISTING_GROUP_ROOT, ) operations = [] if replace_existing_tree existing_listing_group_filters = get_existing_listing_group_filters_in_asset_group( client, customer_id, asset_group_resource_name, ) if existing_listing_group_filters.length > 0 # A special factory object that ensures the creation of remove operations # in the correct order (child listing group filters must be removed # before their parents). remove_operation_factory = AssetGroupListingGroupFilterRemoveOperationFactory.new( existing_listing_group_filters ) operations += remove_operation_factory.remove_all(client) end end operations << create_operation_factory.create_root(client) operations << create_operation_factory.create_unit( client, TEMPORARY_ID_LISTING_GROUP_ROOT, create_operation_factory.next_id, client.resource.listing_group_filter_dimension do |dimension| dimension.product_condition = client.resource.product_condition do |condition| condition.condition = :NEW end end, ) operations << create_operation_factory.create_unit( client, TEMPORARY_ID_LISTING_GROUP_ROOT, create_operation_factory.next_id, client.resource.listing_group_filter_dimension do |dimension| dimension.product_condition = client.resource.product_condition do |condition| condition.condition = :USED end end, ) # We save this ID because we create child nodes underneath it. subdivision_id_condition_other = create_operation_factory.next_id operations << create_operation_factory.create_subdivision( client, TEMPORARY_ID_LISTING_GROUP_ROOT, subdivision_id_condition_other, client.resource.listing_group_filter_dimension do |dimension| dimension.product_condition = client.resource.product_condition do |condition| # All sibling nodes must have the same dimension type. We use an empty # ProductCondition to indicate that this is an "Other" partition. end end, ) operations << create_operation_factory.create_unit( client, subdivision_id_condition_other, create_operation_factory.next_id, client.resource.listing_group_filter_dimension do |dimension| dimension.product_brand = client.resource.product_brand do |brand| brand.value = 'CoolBrand' end end, ) operations << create_operation_factory.create_unit( client, subdivision_id_condition_other, create_operation_factory.next_id, client.resource.listing_group_filter_dimension do |dimension| dimension.product_brand = client.resource.product_brand do |brand| brand.value = 'CheapBrand' end end, ) operations << create_operation_factory.create_unit( client, subdivision_id_condition_other, create_operation_factory.next_id, client.resource.listing_group_filter_dimension do |dimension| dimension.product_brand = client.resource.product_brand do |brand| end end, ) response = client.service.google_ads.mutate( customer_id: customer_id, mutate_operations: operations, ) print_response_details(operations, response) end # Fetches all of the listing group filters in an asset group. def get_existing_listing_group_filters_in_asset_group(client, customer_id, asset_group_resource_name) query = <<~QUERY SELECT asset_group_listing_group_filter.resource_name, asset_group_listing_group_filter.parent_listing_group_filter FROM asset_group_listing_group_filter WHERE asset_group_listing_group_filter.asset_group = '#{asset_group_resource_name}' QUERY response = client.service.google_ads.search( customer_id: customer_id, page_size: 10000, query: query, ) response.map { |row| row.asset_group_listing_group_filter } end def print_response_details(operations, response) response.mutate_operation_responses.each_with_index do |row, i| resource_name = row.asset_group_listing_group_filter_result.resource_name operation_type = operations[i].asset_group_listing_group_filter_operation.operation case operation_type when :create puts "Created AssetGroupListingGroupFilter with resource name '#{resource_name}'." when :remove puts "Removed AssetGroupListingGroupFilter with resource name '#{resource_name}'." else puts "Unsupported operation type #{operation_type}." end end end # A factory that creates MutateOperations for removing an existing tree of # AssetGroupListingGroupFilters. # # AssetGroupListingGroupFilters must be removed in a specific order: all of the # children of a filter must be removed before the filter itself, otherwise the # API will return an error. # # This object is intended to be used with an array of MutateOperations to # perform a series of related updates to an AssetGroup. class AssetGroupListingGroupFilterRemoveOperationFactory def initialize(resources) raise "No listing group filters to remove." if resources.size == 0 # By default, each node only knows about its parents. # However, to remove children first, we need to have a mapping # of parents to children, so we build that here. @parents_to_children = {} resources.each do |filter| parent_resource_name = filter.parent_listing_group_filter if parent_resource_name.nil? || parent_resource_name.empty? if !@root_resource_name.nil? raise "More than one root node." end @root_resource_name = filter.resource_name next end siblings = if @parents_to_children.has_key?(parent_resource_name) @parents_to_children[parent_resource_name] else Set.new end siblings.add(filter.resource_name) @parents_to_children[parent_resource_name] = siblings end end # Creates a list of MutateOperations that remove all of the resources in the # tree originally used to create this factory object. def remove_all(client) remove_descendents_and_filter(client, @root_resource_name) end # Creates a list of MutateOperations that remove all the descendents of the # specified AssetGroupListingGroupFilter resource name. The order of removal # is post-order, where all the children (and their children, recursively) are # removed first. Then, the node itself is removed. def remove_descendents_and_filter(client, resource_name) operations = [] if @parents_to_children.has_key?(resource_name) @parents_to_children[resource_name].each do |child| operations += remove_descendents_and_filter(client, child) end end operations << client.operation.mutate do |m| m.asset_group_listing_group_filter_operation = client.operation.remove_resource.asset_group_listing_group_filter(resource_name) end operations end end # A factory that creates MutateOperations wrapping # AssetGroupListingGroupFilterMutateOperations for a specific customerId and # assetGroupId. # # This object is intended to be used with an array of MutateOperations to # perform an atomic update to an AssetGroup. class AssetGroupListingGroupFilterCreateOperationFactory def initialize(customer_id, asset_group_id, root_listing_group_id) @customer_id = customer_id @asset_group_id = asset_group_id @root_listing_group_id = root_listing_group_id.to_i @next_id = @root_listing_group_id - 1 end # Returns a new temporary ID to be used for a resource name in a # MutateOperation. See # https://developers.google.com/google-ads/api/docs/mutating/best-practices#temporary_resource_names # for details about temporary IDs. def next_id @next_id -= 1 end # Creates a MutateOperation that creates a root AssetGroupListingGroupFilter # for the factory's AssetGroup. # # The root node or partition is the default, which is displayed as "All # Products". def create_root(client) operation = client.operation.create_resource.asset_group_listing_group_filter do |lgf| lgf.resource_name = client.path.asset_group_listing_group_filter( @customer_id, @asset_group_id, @root_listing_group_id, ) lgf.asset_group = client.path.asset_group( @customer_id, @asset_group_id, ) # Since this is the root node, do not set the ParentListingGroupFilter. # For all other nodes, this would refer to the parent listing group # filter resource name. # lgf.parent_listing_group_filter = "<PARENT FILTER NAME>" # Unlike AddPerformanceMaxRetailCampaign, the type for the root node here # must be SUBDIVISION because we add child partitions under it. lgf.type = :SUBDIVISION # Because this is a Performance Max campaign for retail, we need to # specify that this is in the SHOPPING listing source. lgf.listing_source = :SHOPPING end client.operation.mutate do |m| m.asset_group_listing_group_filter_operation = operation end end # Creates a MutateOperation that creates a intermediate # AssetGroupListingGroupFilter for the factory's AssetGroup. # # Use this method if the filter will have child filters. Otherwise, use the # create_unit method. def create_subdivision(client, parent, id, dimension) operation = client.operation.create_resource.asset_group_listing_group_filter do |lgf| lgf.resource_name = client.path.asset_group_listing_group_filter( @customer_id, @asset_group_id, id, ) lgf.asset_group = client.path.asset_group( @customer_id, @asset_group_id, ) lgf.parent_listing_group_filter = client.path.asset_group_listing_group_filter( @customer_id, @asset_group_id, parent, ) # We must use the SUBDIVISION type to indicate that the # AssetGroupListingGroupFilter will have children. lgf.type = :SUBDIVISION # Because this is a Performance Max campaign for retail, we need to # specify that this is in the SHOPPING listing source. lgf.listing_source = :SHOPPING lgf.case_value = dimension end client.operation.mutate do |m| m.asset_group_listing_group_filter_operation = operation end end # Creates a MutateOperation that creates a child AssetGroupListingGroupFilter # for the factory's AssetGroup. # # Use this method if the filter won't have child filters. Otherwise, use the # create_subdivision method. def create_unit(client, parent, id, dimension) operation = client.operation.create_resource.asset_group_listing_group_filter do |lgf| lgf.resource_name = client.path.asset_group_listing_group_filter( @customer_id, @asset_group_id, id, ) lgf.asset_group = client.path.asset_group( @customer_id, @asset_group_id, ) lgf.parent_listing_group_filter = client.path.asset_group_listing_group_filter( @customer_id, @asset_group_id, parent, ) # We must use the UNIT_INCLUDED type to indicate that the # AssetGroupListingGroupFilter won't have children. lgf.type = :UNIT_INCLUDED # Because this is a Performance Max campaign for retail, we need to # specify that this is in the SHOPPING listing source. lgf.listing_source = :SHOPPING lgf.case_value = dimension end client.operation.mutate do |m| m.asset_group_listing_group_filter_operation = operation end end end if __FILE__ == $0 options = {} # The following parameter(s) should be provided to run the example. You can # either specify these by changing the INSERT_XXX_ID_HERE values below, or on # the command line. # # Parameters passed on the command line will override any parameters set in # code. # # Running the example with -h will print the command line usage. options[:customer_id] = 'INSERT_CUSTOMER_ID_HERE' options[:asset_group_id] = 'INSERT_ASSET_GROUP_ID_HERE' options[:replace_existing_tree] = false OptionParser.new do |opts| opts.banner = sprintf('Usage: %s [options]', File.basename(__FILE__)) opts.separator '' opts.separator 'Options:' opts.on('-C', '--customer-id CUSTOMER-ID', String, 'Customer ID') do |v| options[:customer_id] = v end opts.on('-g', '--asset-group-id ASSET-GROUP-ID', String, 'Asset Group ID') do |v| options[:asset_group_id] = v end opts.on('-r', '--replace-existing-tree REPLACE-EXISTING-TREE', String, 'Replace existing tree?') do |v| options[:replace_existing_tree] = true end opts.separator '' opts.separator 'Help:' opts.on_tail('-h', '--help', 'Show this message') do puts opts exit end end.parse! begin add_performance_max_product_listing_group_tree( options.fetch(:customer_id).tr("-", ""), options.fetch(:asset_group_id), options.fetch(:replace_existing_tree), ) rescue Google::Ads::GoogleAds::Errors::GoogleAdsError => e e.failure.errors.each do |error| STDERR.printf("Error with message: %s\n", error.message) if error.location error.location.field_path_elements.each do |field_path_element| STDERR.printf("\tOn field: %s\n", field_path_element.field_name) end end error.error_code.to_h.each do |k, v| next if v == :UNSPECIFIED STDERR.printf("\tType: %s\n\tCode: %s\n", k, v) end end raise end end
Perl
#!/usr/bin/perl -w # # Copyright 2022, Google LLC # # 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 shows how to add product partitions to a Performance Max retail campaign. # # For Performance Max campaigns, product partitions are represented using the # AssetGroupListingGroupFilter resource. This resource can be combined with itself # to form a hierarchy that creates a product partition tree. # # For more information about Performance Max retail campaigns, see the # add_performance_max_retail_campaign example. use strict; use warnings; use utf8; use FindBin qw($Bin); use lib "$Bin/../../lib"; use Google::Ads::GoogleAds::Client; use Google::Ads::GoogleAds::Utils::GoogleAdsHelper; use Google::Ads::GoogleAds::V17::Resources::AssetGroupListingGroupFilter; use Google::Ads::GoogleAds::V17::Resources::ListingGroupFilterDimension; use Google::Ads::GoogleAds::V17::Resources::ProductCondition; use Google::Ads::GoogleAds::V17::Resources::ProductBrand; use Google::Ads::GoogleAds::V17::Enums::ListingGroupFilterTypeEnum qw(SUBDIVISION UNIT_INCLUDED); use Google::Ads::GoogleAds::V17::Enums::ListingGroupFilterListingSourceEnum qw(SHOPPING); use Google::Ads::GoogleAds::V17::Enums::ListingGroupFilterProductConditionEnum qw(NEW USED); use Google::Ads::GoogleAds::V17::Services::GoogleAdsService::MutateOperation; use Google::Ads::GoogleAds::V17::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation; use Google::Ads::GoogleAds::V17::Utils::ResourceNames; use Getopt::Long qw(:config auto_help); use Pod::Usage; use Cwd qw(abs_path); # The following parameter(s) should be provided to run the example. You can # either specify these by changing the INSERT_XXX_ID_HERE values below, or on # the command line. # # Parameters passed on the command line will override any parameters set in # code. # # Running the example with -h will print the command line usage. my $customer_id = "INSERT_CUSTOMER_ID_HERE"; my $asset_group_id = "INSERT_ASSET_GROUP_ID_HERE"; # Optional: Removes the existing listing group tree from the asset group or not. # # If the current asset group already has a tree of listing group filters, and you # try to add a new set of listing group filters including a root filter, you'll # receive a 'ASSET_GROUP_LISTING_GROUP_FILTER_ERROR_MULTIPLE_ROOTS' error. # # Setting this option to a defined value will remove the existing tree and prevent # this error. my $replace_existing_tree = undef; # We specify temporary IDs that are specific to a single mutate request. # Temporary IDs are always negative and unique within one mutate request. use constant LISTING_GROUP_ROOT_TEMPORARY_ID => -1; sub add_performance_max_product_listing_group_tree { my ($api_client, $customer_id, $asset_group_id, $replace_existing_tree) = @_; # We create all the mutate operations that manipulate a specific asset group for # a specific customer. The operations are used to optionally remove all asset # group listing group filters from the tree, and then to construct a new tree # of filters. These filters can have a parent-child relationship, and also include # a special root that includes all children. # # When creating these filters, we use temporary IDs to create the hierarchy between # the root listing group filter, and the subdivisions and leave nodes beneath that. my $mutate_operations = []; if (defined $replace_existing_tree) { my $existing_listing_group_filters = get_all_existing_listing_group_filter_assets_in_asset_group( $api_client, $customer_id, Google::Ads::GoogleAds::V17::Utils::ResourceNames::asset_group( $customer_id, $asset_group_id )); if (scalar @$existing_listing_group_filters > 0) { push @$mutate_operations, # Ensure the creation of remove operations in the correct order (child # listing group filters must be removed before their parents). @{ create_mutate_operations_for_removing_listing_group_filters_tree( $existing_listing_group_filters)}; } } push @$mutate_operations, create_mutate_operation_for_root($customer_id, $asset_group_id, LISTING_GROUP_ROOT_TEMPORARY_ID); # The temporary ID to be used for creating subdivisions and units. my $temp_id = LISTING_GROUP_ROOT_TEMPORARY_ID - 1; push @$mutate_operations, create_mutate_operation_for_unit( $customer_id, $asset_group_id, $temp_id--, LISTING_GROUP_ROOT_TEMPORARY_ID, Google::Ads::GoogleAds::V17::Resources::ListingGroupFilterDimension->new({ productCondition => Google::Ads::GoogleAds::V17::Resources::ProductCondition->new({ condition => NEW })})); push @$mutate_operations, create_mutate_operation_for_unit( $customer_id, $asset_group_id, $temp_id--, LISTING_GROUP_ROOT_TEMPORARY_ID, Google::Ads::GoogleAds::V17::Resources::ListingGroupFilterDimension->new({ productCondition => Google::Ads::GoogleAds::V17::Resources::ProductCondition->new({ condition => USED })})); # We save this ID to create child nodes underneath it. my $condition_other_subdivision_id = $temp_id--; # We're calling create_mutate_operation_for_subdivision() because this listing # group will have children. push @$mutate_operations, create_mutate_operation_for_subdivision( $customer_id, $asset_group_id, $condition_other_subdivision_id, LISTING_GROUP_ROOT_TEMPORARY_ID, Google::Ads::GoogleAds::V17::Resources::ListingGroupFilterDimension->new({ # All sibling nodes must have the same dimension type. We use an empty # ProductCondition to indicate that this is an "Other" partition. productCondition => Google::Ads::GoogleAds::V17::Resources::ProductCondition->new({})})); push @$mutate_operations, create_mutate_operation_for_unit( $customer_id, $asset_group_id, $temp_id--, $condition_other_subdivision_id, Google::Ads::GoogleAds::V17::Resources::ListingGroupFilterDimension->new({ productBrand => Google::Ads::GoogleAds::V17::Resources::ProductBrand->new({ value => "CoolBrand" })})); push @$mutate_operations, create_mutate_operation_for_unit( $customer_id, $asset_group_id, $temp_id--, $condition_other_subdivision_id, Google::Ads::GoogleAds::V17::Resources::ListingGroupFilterDimension->new({ productBrand => Google::Ads::GoogleAds::V17::Resources::ProductBrand->new({ value => "CheapBrand" })})); push @$mutate_operations, create_mutate_operation_for_unit( $customer_id, $asset_group_id, $temp_id--, $condition_other_subdivision_id, # All other product brands. Google::Ads::GoogleAds::V17::Resources::ListingGroupFilterDimension->new({ productBrand => Google::Ads::GoogleAds::V17::Resources::ProductBrand->new({})})); # Issue a mutate request to create everything and print its information. my $response = $api_client->GoogleAdsService()->mutate({ customerId => $customer_id, mutateOperations => $mutate_operations }); print_response_details($mutate_operations, $response); return 1; } # Fetches all of the asset group listing group filters in an asset group. sub get_all_existing_listing_group_filter_assets_in_asset_group { my ($api_client, $customer_id, $asset_group_resource_name) = @_; # Create a query that retrieves asset group listing group filters. # The limit to the number of listing group filters permitted in a Performance # Max campaign can be found here: # https://developers.google.com/google-ads/api/docs/best-practices/system-limits. my $query = sprintf "SELECT asset_group_listing_group_filter.resource_name, " . "asset_group_listing_group_filter.parent_listing_group_filter " . "FROM asset_group_listing_group_filter " . "WHERE asset_group_listing_group_filter.asset_group = '%s'", $asset_group_resource_name; # Issue a search request by specifying page size. my $response = $api_client->GoogleAdsService()->search({ customerId => $customer_id, query => $query }); my $asset_group_listing_group_filters = []; # Iterate over all rows in all pages to get an asset group listing group filter. foreach my $google_ads_row (@{$response->{results}}) { push @$asset_group_listing_group_filters, $google_ads_row->{assetGroupListingGroupFilter}; } return $asset_group_listing_group_filters; } # Creates mutate operations for removing an existing tree of asset group listing # group filters. # # Asset group listing group filters must be removed in a specific order: all of # the children of a filter must be removed before the filter itself, otherwise # the API will return an error. sub create_mutate_operations_for_removing_listing_group_filters_tree { my ($asset_group_listing_group_filters) = @_; if (scalar @$asset_group_listing_group_filters == 0) { die "No listing group filters to remove."; } my $resource_names_to_listing_group_filters = {}; my $parents_to_children = {}; my $root_resource_name = undef; foreach my $asset_group_listing_group_filter (@$asset_group_listing_group_filters) { $resource_names_to_listing_group_filters-> {$asset_group_listing_group_filter->{resourceName}} = $asset_group_listing_group_filter; # When the node has no parent, it means it's the root node, which is treated # differently. if (!defined $asset_group_listing_group_filter->{parentListingGroupFilter}) { if (defined $root_resource_name) { die "More than one root node found."; } $root_resource_name = $asset_group_listing_group_filter->{resourceName}; next; } my $parent_resource_name = $asset_group_listing_group_filter->{parentListingGroupFilter}; my $siblings = []; # Check to see if we've already visited a sibling in this group and fetch it. if (exists $parents_to_children->{$parent_resource_name}) { $siblings = $parents_to_children->{$parent_resource_name}; } push @$siblings, $asset_group_listing_group_filter->{resourceName}; $parents_to_children->{$parent_resource_name} = $siblings; } return create_mutate_operations_for_removing_descendents($root_resource_name, $parents_to_children); } # Creates a list of mutate operations that remove all the descendents of the # specified asset group listing group filter's resource name. The order of removal # is post-order, where all the children (and their children, recursively) are # removed first. Then, the node itself is removed. sub create_mutate_operations_for_removing_descendents { my ($asset_group_listing_group_filter_resource_name, $parents_to_children) = @_; my $operations = []; if ( exists $parents_to_children-> {$asset_group_listing_group_filter_resource_name}) { foreach my $child ( @{$parents_to_children->{$asset_group_listing_group_filter_resource_name}} ) { push @$operations, @{ create_mutate_operations_for_removing_descendents($child, $parents_to_children)}; } } push @$operations, Google::Ads::GoogleAds::V17::Services::GoogleAdsService::MutateOperation-> new({ assetGroupListingGroupFilterOperation => Google::Ads::GoogleAds::V17::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation ->new({ remove => $asset_group_listing_group_filter_resource_name })}); return $operations; } # Creates a mutate operation that creates a root asset group listing group filter # for the factory's asset group. # # The root node or partition is the default, which is displayed as "All Products". sub create_mutate_operation_for_root { my ($customer_id, $asset_group_id, $root_listing_group_id) = @_; my $asset_group_listing_group_filter = Google::Ads::GoogleAds::V17::Resources::AssetGroupListingGroupFilter->new({ resourceName => Google::Ads::GoogleAds::V17::Utils::ResourceNames::asset_group_listing_group_filter( $customer_id, $asset_group_id, $root_listing_group_id ), assetGroup => Google::Ads::GoogleAds::V17::Utils::ResourceNames::asset_group( $customer_id, $asset_group_id ), # Since this is the root node, do not set the 'parentListingGroupFilter' field. # For all other nodes, this would refer to the parent listing group filter # resource name. # Unlike add_performance_max_retail_campaign, the type for the root node # here must be SUBDIVISION because we add child partitions under it. type => SUBDIVISION, # Because this is a Performance Max campaign for retail, we need to specify # that this is in the shopping listing source. listingSource => SHOPPING }); return Google::Ads::GoogleAds::V17::Services::GoogleAdsService::MutateOperation-> new({ assetGroupListingGroupFilterOperation => Google::Ads::GoogleAds::V17::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation ->new({ create => $asset_group_listing_group_filter })}); } # Creates a mutate operation that creates a intermediate asset group listing group filter. sub create_mutate_operation_for_subdivision { my ($customer_id, $asset_group_id, $asset_group_listing_group_filter_id, $parent_id, $listing_group_filter_dimension) = @_; my $asset_group_listing_group_filter = Google::Ads::GoogleAds::V17::Resources::AssetGroupListingGroupFilter->new({ resourceName => Google::Ads::GoogleAds::V17::Utils::ResourceNames::asset_group_listing_group_filter( $customer_id, $asset_group_id, $asset_group_listing_group_filter_id ), assetGroup => Google::Ads::GoogleAds::V17::Utils::ResourceNames::asset_group( $customer_id, $asset_group_id ), # Set the type as a SUBDIVISION, which will allow the node to be the parent # of another sub-tree. type => SUBDIVISION, # Because this is a Performance Max campaign for retail, we need to specify # that this is in the shopping listing source. listingSource => SHOPPING, parentListingGroupFilter => Google::Ads::GoogleAds::V17::Utils::ResourceNames::asset_group_listing_group_filter( $customer_id, $asset_group_id, $parent_id ), # Case values contain the listing dimension used for the node. caseValue => $listing_group_filter_dimension }); return Google::Ads::GoogleAds::V17::Services::GoogleAdsService::MutateOperation-> new({ assetGroupListingGroupFilterOperation => Google::Ads::GoogleAds::V17::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation ->new({ create => $asset_group_listing_group_filter })}); } # Creates a mutate operation that creates a child asset group listing group filter # (unit node). # # Use this method if the filter won't have child filters. Otherwise, use # create_mutate_operation_for_subdivision(). sub create_mutate_operation_for_unit { my ($customer_id, $asset_group_id, $asset_group_listing_group_filter_id, $parent_id, $listing_group_filter_dimension) = @_; my $asset_group_listing_group_filter = Google::Ads::GoogleAds::V17::Resources::AssetGroupListingGroupFilter->new({ resourceName => Google::Ads::GoogleAds::V17::Utils::ResourceNames::asset_group_listing_group_filter( $customer_id, $asset_group_id, $asset_group_listing_group_filter_id ), assetGroup => Google::Ads::GoogleAds::V17::Utils::ResourceNames::asset_group( $customer_id, $asset_group_id ), parentListingGroupFilter => Google::Ads::GoogleAds::V17::Utils::ResourceNames::asset_group_listing_group_filter( $customer_id, $asset_group_id, $parent_id ), # Set the type as a UNIT_INCLUDED to indicate that this asset group listing # group filter won't have children. type => UNIT_INCLUDED, # Because this is a Performance Max campaign for retail, we need to specify # that this is in the shopping listing source. listingSource => SHOPPING, caseValue => $listing_group_filter_dimension }); return Google::Ads::GoogleAds::V17::Services::GoogleAdsService::MutateOperation-> new({ assetGroupListingGroupFilterOperation => Google::Ads::GoogleAds::V17::Services::AssetGroupListingGroupFilterService::AssetGroupListingGroupFilterOperation ->new({ create => $asset_group_listing_group_filter })}); } # Prints the details of a mutate google ads response. Parses the "response" oneof # field name and uses it to extract the new entity's name and resource name. sub print_response_details { my ($mutate_operations, $mutate_google_ads_response) = @_; while (my ($i, $operation_response) = each @{$mutate_google_ads_response->{mutateOperationResponses}}) { if (!exists $operation_response->{assetGroupListingGroupFilterResult}) { # Trim the substring "Result" from the end of the entity name. my $result_type = [keys %$operation_response]->[0]; printf "Unsupported entity type: %s.\n", $result_type =~ s/Result$//r; next; } my $operation = $mutate_operations->[$i]{assetGroupListingGroupFilterOperation}; if (exists $operation->{create}) { printf "Created an asset group listing group filter with resource name: " . "'%s'.\n", $operation_response->{assetGroupListingGroupFilterResult}{resourceName}; } elsif (exists $operation->{remove}) { printf "Removed an asset group listing group filter with resource name: " . "'%s'.\n", $operation_response->{assetGroupListingGroupFilterResult}{resourceName}; } else { printf "Unsupported operation type: '%s'.\n", [keys %$operation]->[0]; } } } # Don't run the example if the file is being included. if (abs_path($0) ne abs_path(__FILE__)) { return 1; } # Get Google Ads Client, credentials will be read from ~/googleads.properties. my $api_client = Google::Ads::GoogleAds::Client->new(); # By default examples are set to die on any server returned fault. $api_client->set_die_on_faults(1); # Parameters passed on the command line will override any parameters set in code. GetOptions( "customer_id=s" => \$customer_id, "asset_group_id=i" => \$asset_group_id, "replace_existing_tree=s" => \$replace_existing_tree ); # Print the help message if the parameters are not initialized in the code nor # in the command line. pod2usage(2) if not check_params($customer_id, $asset_group_id); # Call the example. add_performance_max_product_listing_group_tree( $api_client, $customer_id =~ s/-//gr, $asset_group_id, $replace_existing_tree ); =pod =head1 NAME add_performance_max_product_listing_group_tree =head1 DESCRIPTION This example shows how to add product partitions to a Performance Max retail campaign. For Performance Max campaigns, product partitions are represented using the AssetGroupListingGroupFilter resource. This resource can be combined with itself to form a hierarchy that creates a product partition tree. For more information about Performance Max retail campaigns, see the add_performance_max_retail_campaign example. =head1 SYNOPSIS add_performance_max_product_listing_group_tree.pl [options] -help Show the help message. -customer_id The Google Ads customer ID. -asset_group_id The asset group ID. -replace_existing_tree [optional] Whether it should replace the existing listing group tree on an asset group. =cut