From 9c04c96363487a09b65555fa93dde73d941d97ec Mon Sep 17 00:00:00 2001 From: Sarah Date: Wed, 3 Jun 2026 16:20:32 -0400 Subject: [PATCH 1/4] Add experiments examples --- .../CreateSearchCustomExperiment.java} | 28 +- .../experiments/GetExperimentPerformance.java | 266 ++++++++++++++++++ .../examples/utils/ArgumentNames.java | 1 + 3 files changed, 286 insertions(+), 9 deletions(-) rename google-ads-examples/src/main/java/com/google/ads/googleads/examples/{campaignmanagement/CreateExperiment.java => experiments/CreateSearchCustomExperiment.java} (88%) create mode 100644 google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/GetExperimentPerformance.java diff --git a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/campaignmanagement/CreateExperiment.java b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java similarity index 88% rename from google-ads-examples/src/main/java/com/google/ads/googleads/examples/campaignmanagement/CreateExperiment.java rename to google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java index 03dcd7772..27b1b46ca 100644 --- a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/campaignmanagement/CreateExperiment.java +++ b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.ads.googleads.examples.campaignmanagement; +package com.google.ads.googleads.examples.experiments; import static com.google.ads.googleads.examples.utils.CodeSampleHelper.getPrintableDateTime; @@ -47,12 +47,19 @@ import java.util.List; /** - * This example creates a new experiment, experiment arms, and demonstrates how to modify the draft - * campaign as well as begin the experiment. + * This example creates a standard, system-managed campaign experiment. + * + *

It demonstrates how to create an experiment, configure its control and treatment arms (where + * the treatment arm automatically generates a draft campaign), modify the system-generated draft + * campaign, and schedule the experiment. + * + *

Note: This standard draft-based workflow does not apply to all experiment types (e.g., + * hotswap or asset optimization experiments) that do not use system-generated treatment campaign + * copies. */ -public class CreateExperiment { +public class CreateSearchCustomExperiment { - private static class CreateExperimentParams extends CodeSampleParams { + private static class CreateSearchCustomExperimentParams extends CodeSampleParams { @Parameter(names = ArgumentNames.CUSTOMER_ID, required = true) private Long customerId; @@ -62,7 +69,7 @@ private static class CreateExperimentParams extends CodeSampleParams { } public static void main(String[] args) { - CreateExperimentParams params = new CreateExperimentParams(); + CreateSearchCustomExperimentParams params = new CreateSearchCustomExperimentParams(); if (!params.parseArguments(args)) { throw new IllegalArgumentException("Invalid or missing command line arguments"); } @@ -80,7 +87,7 @@ public static void main(String[] args) { } try { - new CreateExperiment().runExample(googleAdsClient, params.customerId, params.baseCampaignId); + new CreateSearchCustomExperiment().runExample(googleAdsClient, params.customerId, params.baseCampaignId); } 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 @@ -130,6 +137,9 @@ private String createExperimentResource(GoogleAdsClient googleAdsClient, long cu Experiment.newBuilder() // Name must be unique. .setName("Example Experiment #" + getPrintableDateTime()) + // We specify SEARCH_CUSTOM to create a standard search campaign experiment. + // This type uses a standard draft-based workflow where the system automatically + // creates a draft/in-design campaign for the treatment arm. .setType(ExperimentType.SEARCH_CUSTOM) .setSuffix("[experiment]") .setStatus(ExperimentStatus.SETUP) @@ -170,8 +180,8 @@ private String createExperimentArms( operations.add( ExperimentArmOperation.newBuilder() .setCreate( - // The non-"control" arm, also called a "treatment" arm, will automatically - // generate draft campaigns that you can modify before starting the experiment. + // In standard campaign experiments, creating the treatment arm automatically + // generates a draft campaign that you can modify before starting the experiment. ExperimentArm.newBuilder() .setControl(false) .setExperiment(experiment) diff --git a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/GetExperimentPerformance.java b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/GetExperimentPerformance.java new file mode 100644 index 000000000..b45641ab8 --- /dev/null +++ b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/GetExperimentPerformance.java @@ -0,0 +1,266 @@ +package com.google.ads.googleads.examples.experiments; + +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.v24.enums.BudgetDeliveryMethodEnum.BudgetDeliveryMethod; +import com.google.ads.googleads.v24.enums.ExperimentTypeEnum.ExperimentType; +import com.google.ads.googleads.v24.errors.GoogleAdsError; +import com.google.ads.googleads.v24.errors.GoogleAdsException; +import com.google.ads.googleads.v24.resources.CampaignBudget; +import com.google.ads.googleads.v24.common.Metrics; +import com.google.ads.googleads.v24.services.CampaignBudgetMapping; +import com.google.ads.googleads.v24.services.CampaignBudgetOperation; +import com.google.ads.googleads.v24.services.CampaignBudgetServiceClient; +import com.google.ads.googleads.v24.services.ExperimentServiceClient; +import com.google.ads.googleads.v24.services.GoogleAdsRow; +import com.google.ads.googleads.v24.services.GoogleAdsServiceClient; +import com.google.ads.googleads.v24.services.MutateCampaignBudgetsResponse; +import com.google.ads.googleads.v24.services.SearchGoogleAdsRequest; +import com.google.api.gax.longrunning.OperationFuture; +import com.google.protobuf.Empty; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Collections; +import java.util.UUID; + +/** + * This example illustrates how to retrieve performance metrics for an experiment. + * + *

It shows how to query statistical significance metrics for the experiment, + * and how to execute actions such as promoting, ending, or graduating an experiment. + */ +public class GetExperimentPerformance { + private static final double P_VALUE_THRESHOLD = 0.05; + + private static class GetExperimentPerformanceParams extends CodeSampleParams { + + @Parameter(names = ArgumentNames.CUSTOMER_ID, required = true) + private Long customerId; + + @Parameter(names = ArgumentNames.EXPERIMENT_ID, required = true) + private Long experimentId; + } + + public static void main(String[] args) { + GetExperimentPerformanceParams params = new GetExperimentPerformanceParams(); + if (!params.parseArguments(args)) { + throw new IllegalArgumentException("Invalid or missing command line arguments"); + } + + 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 GetExperimentPerformance().runExample(googleAdsClient, params.customerId, params.experimentId); + } catch (GoogleAdsException gae) { + 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); + } + } + + /** + * Runs the example. + * + * @param googleAdsClient the googleAdsClient. + * @param customerId the customer ID. + * @param experimentId the experiment ID. + */ + private void runExample(GoogleAdsClient googleAdsClient, long customerId, long experimentId) { + try (GoogleAdsServiceClient googleAdsServiceClient = + googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) { + + String query = + String.format( + "SELECT " + + "experiment.resource_name, " + + "experiment.name, " + + "experiment.experiment_id, " + + "experiment.type, " + + "metrics.conversions_absolute_change_p_value, " + + "metrics.conversions_absolute_change_point_estimate, " + + "metrics.conversions_absolute_change_margin_of_error, " + + "metrics.clicks_p_value, " + + "metrics.clicks_point_estimate, " + + "metrics.clicks_margin_of_error " + + "FROM experiment " + + "WHERE experiment.experiment_id = %d", + experimentId); + + SearchGoogleAdsRequest request = + SearchGoogleAdsRequest.newBuilder() + .setCustomerId(Long.toString(customerId)) + .setQuery(query) + .build(); + + GoogleAdsServiceClient.SearchPagedResponse response = googleAdsServiceClient.search(request); + + boolean experimentFound = false; + for (GoogleAdsRow row : response.iterateAll()) { + experimentFound = true; + System.out.printf("Found experiment: %s%n", row.getExperiment().getName()); + System.out.printf(" Resource Name: %s%n", row.getExperiment().getResourceName()); + + evaluateExperiment(googleAdsClient, customerId, row); + } + + if (!experimentFound) { + System.out.printf("No experiment found for experiment ID: %d%n", experimentId); + } + } + } + + // [START get_experiment_performance_1] + private void evaluateExperiment(GoogleAdsClient googleAdsClient, long customerId, GoogleAdsRow row) { + Metrics metrics = row.getMetrics(); + String experimentResourceName = row.getExperiment().getResourceName(); + + double convPValue = metrics.getConversionsAbsoluteChangePValue(); + double convLift = metrics.getConversionsAbsoluteChangePointEstimate(); + double convError = metrics.getConversionsAbsoluteChangeMarginOfError(); + double convLowerBound = convLift - convError; + + if (convPValue <= P_VALUE_THRESHOLD) { + if (convLowerBound > 0) { + System.out.printf( + "Significant Success: Conversions increased. Even at the lower bound, the lift is %.2f. Promoting changes.%n", + convLowerBound); + promoteExperiment(googleAdsClient, customerId, experimentResourceName); + return; + } else if ((convLift + convError) < 0) { + System.out.printf( + "Significant Decline: Even the upper bound (%.2f) is below zero. Ending experiment.%n", + convLift + convError); + endExperiment(googleAdsClient, customerId, experimentResourceName); + return; + } + } + + double clickPValue = metrics.getClicksPValue(); + double clickLift = metrics.getClicksPointEstimate(); + double clickError = metrics.getClicksMarginOfError(); + double clickLowerBound = clickLift - clickError; + + if (clickPValue <= P_VALUE_THRESHOLD && clickLowerBound > 0) { + System.out.printf( + "Click volume is significantly up (+%.1f%%). Graduating treatment for further manual analysis.%n", + clickLift * 100); + + ExperimentType experimentType = row.getExperiment().getType(); + if (experimentType != ExperimentType.ADOPT_BROAD_MATCH_KEYWORDS + && experimentType != ExperimentType.ADOPT_AI_MAX) { + graduateExperiment(googleAdsClient, customerId, experimentResourceName); + } else { + System.out.println( + "Intra-campaign trial detected: Graduation is not supported because there is only one campaign. Continuing to run to gather more conversion data."); + } + } else { + System.out.printf( + "Inconclusive: No significant lift in Conversions (p=%.2f) or Clicks (p=%.2f). Current estimated lift: %.2f +/- %.2f. Continue running.%n", + convPValue, clickPValue, convLift, convError); + } + } + // [END get_experiment_performance_1] + + private void promoteExperiment(GoogleAdsClient googleAdsClient, long customerId, String experimentResourceName) { + try (ExperimentServiceClient experimentServiceClient = + googleAdsClient.getLatestVersion().createExperimentServiceClient()) { + OperationFuture operationResponse = + experimentServiceClient.promoteExperimentAsync(experimentResourceName); + System.out.printf("Started promotion for experiment: %s%n", experimentResourceName); + System.out.printf( + "The promotion is running asynchronously. You can track its progress using the long-running operation: %s%n", + operationResponse.getName()); + System.out.println( + "Best Practice: If the promotion fails, you can retrieve the full list of errors by calling ExperimentService.ListExperimentAsyncErrors."); + } catch (Exception e) { + System.out.printf("Failed to promote experiment: %s%n", e.getMessage()); + } + } + + private void endExperiment(GoogleAdsClient googleAdsClient, long customerId, String experimentResourceName) { + try (ExperimentServiceClient experimentServiceClient = + googleAdsClient.getLatestVersion().createExperimentServiceClient()) { + experimentServiceClient.endExperiment(experimentResourceName); + System.out.printf("Successfully ended experiment: %s%n", experimentResourceName); + } + } + + private void graduateExperiment(GoogleAdsClient googleAdsClient, long customerId, String experimentResourceName) { + String budgetResourceName; + try (CampaignBudgetServiceClient campaignBudgetServiceClient = + googleAdsClient.getLatestVersion().createCampaignBudgetServiceClient()) { + CampaignBudget campaignBudget = + CampaignBudget.newBuilder() + .setName("Graduated Experiment Budget #" + UUID.randomUUID()) + .setAmountMicros(50_000_000L) + .setDeliveryMethod(BudgetDeliveryMethod.STANDARD) + .build(); + + CampaignBudgetOperation operation = + CampaignBudgetOperation.newBuilder().setCreate(campaignBudget).build(); + + MutateCampaignBudgetsResponse response = + campaignBudgetServiceClient.mutateCampaignBudgets( + Long.toString(customerId), Collections.singletonList(operation)); + budgetResourceName = response.getResults(0).getResourceName(); + System.out.printf( + "Created new standalone campaign budget with resource name: %s%n", budgetResourceName); + } + + String treatmentCampaignResourceName = null; + try (GoogleAdsServiceClient googleAdsServiceClient = + googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) { + String query = + String.format( + "SELECT experiment_arm.campaigns FROM experiment_arm WHERE experiment_arm.experiment = '%s' AND experiment_arm.control = FALSE", + experimentResourceName); + + GoogleAdsServiceClient.SearchPagedResponse searchResponse = + googleAdsServiceClient.search(Long.toString(customerId), query); + + for (GoogleAdsRow row : searchResponse.iterateAll()) { + if (row.getExperimentArm().getCampaignsCount() > 0) { + treatmentCampaignResourceName = row.getExperimentArm().getCampaigns(0); + break; + } + } + } + + if (treatmentCampaignResourceName == null) { + System.out.println("Could not find the treatment campaign associated with this experiment."); + return; + } + + try (ExperimentServiceClient experimentServiceClient = + googleAdsClient.getLatestVersion().createExperimentServiceClient()) { + CampaignBudgetMapping budgetMapping = + CampaignBudgetMapping.newBuilder() + .setExperimentCampaign(treatmentCampaignResourceName) + .setCampaignBudget(budgetResourceName) + .build(); + + experimentServiceClient.graduateExperiment( + experimentResourceName, Collections.singletonList(budgetMapping)); + System.out.printf( + "Successfully graduated experiment campaign %s with new budget %s%n", + treatmentCampaignResourceName, budgetResourceName); + } + } +} diff --git a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/utils/ArgumentNames.java b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/utils/ArgumentNames.java index bc2cfd5c7..a4bb3a63c 100644 --- a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/utils/ArgumentNames.java +++ b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/utils/ArgumentNames.java @@ -58,6 +58,7 @@ public final class ArgumentNames { public static final String CUSTOMER_IDS = "--customerIds"; public static final String EMAIL_ADDRESS = "--emailAddress"; public static final String END_DATE_TIME = "--endDateTime"; + public static final String EXPERIMENT_ID = "--experimentId"; public static final String EXTERNAL_ID = "--externalId"; public static final String FINAL_URL = "--finalUrl"; public static final String FREE_FORM_KEYWORD_TEXT = "--freeFormKeywordText"; From e53bb14747071a06ffddf755acac058e0b80fee8 Mon Sep 17 00:00:00 2001 From: Sarah Date: Wed, 3 Jun 2026 16:37:02 -0400 Subject: [PATCH 2/4] Fix comment --- .../examples/experiments/CreateSearchCustomExperiment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java index 27b1b46ca..b9c854df0 100644 --- a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java +++ b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java @@ -54,7 +54,7 @@ * campaign, and schedule the experiment. * *

Note: This standard draft-based workflow does not apply to all experiment types (e.g., - * hotswap or asset optimization experiments) that do not use system-generated treatment campaign + * intra-campaign or asset optimization experiments) that do not use system-generated treatment campaign * copies. */ public class CreateSearchCustomExperiment { From eff32670363c547686748514b825be88bb9de73e Mon Sep 17 00:00:00 2001 From: Sarah Date: Fri, 5 Jun 2026 16:31:16 -0400 Subject: [PATCH 3/4] Add naming and comment changes from python PR #1078 --- .../CreateSearchCustomExperiment.java | 59 ++++---- ....java => EvaluateAndUpdateExperiment.java} | 127 +++++++++++++----- 2 files changed, 123 insertions(+), 63 deletions(-) rename google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/{GetExperimentPerformance.java => EvaluateAndUpdateExperiment.java} (65%) diff --git a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java index b9c854df0..7dd0fb1b7 100644 --- a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java +++ b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java @@ -47,15 +47,15 @@ import java.util.List; /** - * This example creates a standard, system-managed campaign experiment. + * Creates a standard, system-managed campaign experiment of type SEARCH_CUSTOM. * - *

It demonstrates how to create an experiment, configure its control and treatment arms (where - * the treatment arm automatically generates a draft campaign), modify the system-generated draft - * campaign, and schedule the experiment. + *

Sets up the experiment, configures its control and treatment arms (where the treatment arm + * automatically generates a draft campaign), modifies the system-generated draft campaign, and + * schedule the experiment. * - *

Note: This standard draft-based workflow does not apply to all experiment types (e.g., - * intra-campaign or asset optimization experiments) that do not use system-generated treatment campaign - * copies. + *

Note: This standard draft-based workflow applies only to experiment types that use + * system-generated treatment campaign copies, and excludes intra-campaign or asset-optimization + * experiments. */ public class CreateSearchCustomExperiment { @@ -87,7 +87,8 @@ public static void main(String[] args) { } try { - new CreateSearchCustomExperiment().runExample(googleAdsClient, params.customerId, params.baseCampaignId); + new CreateSearchCustomExperiment() + .runExample(googleAdsClient, params.customerId, params.baseCampaignId); } 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 @@ -126,9 +127,7 @@ private void runExample(GoogleAdsClient googleAdsClient, long customerId, long b } } - /** - * Creates a campaign experiment. - */ + /** Creates a campaign experiment. */ // [START create_experiment_1] private String createExperimentResource(GoogleAdsClient googleAdsClient, long customerId) { ExperimentOperation operation = @@ -156,11 +155,10 @@ private String createExperimentResource(GoogleAdsClient googleAdsClient, long cu return experiment; } } + // [END create_experiment_1] - /** - * Creates control and experiment arms for the experiment. - */ + /** Creates control and experiment arms for the experiment. */ // [START create_experiment_2] private String createExperimentArms( GoogleAdsClient googleAdsClient, long customerId, long campaignId, String experiment) { @@ -193,13 +191,15 @@ private String createExperimentArms( try (ExperimentArmServiceClient experimentArmServiceClient = googleAdsClient.getLatestVersion().createExperimentArmServiceClient()) { // Constructs the mutate request. - MutateExperimentArmsRequest mutateRequest = MutateExperimentArmsRequest.newBuilder() - .setCustomerId(Long.toString(customerId)) - .addAllOperations(operations) - // We want to fetch the draft campaign IDs from the treatment arm, so the easiest way to do - // that is to have the response return the newly created entities. - .setResponseContentType(ResponseContentType.MUTABLE_RESOURCE) - .build(); + MutateExperimentArmsRequest mutateRequest = + MutateExperimentArmsRequest.newBuilder() + .setCustomerId(Long.toString(customerId)) + .addAllOperations(operations) + // We want to fetch the draft campaign IDs from the treatment arm, so the easiest way + // to do + // that is to have the response return the newly created entities. + .setResponseContentType(ResponseContentType.MUTABLE_RESOURCE) + .build(); // Sends the mutate request. MutateExperimentArmsResponse response = @@ -210,22 +210,21 @@ private String createExperimentArms( // treatment arm, you can always filter the query in the next section with // `experiment_arm.control = false`. MutateExperimentArmResult controlArmResult = response.getResults(0); - MutateExperimentArmResult treatmentArmResult = response.getResults( - response.getResultsCount() - 1); + MutateExperimentArmResult treatmentArmResult = + response.getResults(response.getResultsCount() - 1); - System.out.printf("Created control arm with resource name '%s'%n", - controlArmResult.getResourceName()); - System.out.printf("Created treatment arm with resource name '%s'%n", - treatmentArmResult.getResourceName()); + System.out.printf( + "Created control arm with resource name '%s'%n", controlArmResult.getResourceName()); + System.out.printf( + "Created treatment arm with resource name '%s'%n", treatmentArmResult.getResourceName()); return treatmentArmResult.getExperimentArm().getInDesignCampaigns(0); } } + // [END create_experiment_2] - /** - * Modifies the draft campaign. - */ + /** Modifies the draft campaign. */ // [START create_experiment_4] private void modifyDraftCampaign( GoogleAdsClient googleAdsClient, long customerId, String draftCampaign) { diff --git a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/GetExperimentPerformance.java b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/EvaluateAndUpdateExperiment.java similarity index 65% rename from google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/GetExperimentPerformance.java rename to google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/EvaluateAndUpdateExperiment.java index b45641ab8..73a87331a 100644 --- a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/GetExperimentPerformance.java +++ b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/EvaluateAndUpdateExperiment.java @@ -4,12 +4,12 @@ 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.v24.common.Metrics; import com.google.ads.googleads.v24.enums.BudgetDeliveryMethodEnum.BudgetDeliveryMethod; import com.google.ads.googleads.v24.enums.ExperimentTypeEnum.ExperimentType; import com.google.ads.googleads.v24.errors.GoogleAdsError; import com.google.ads.googleads.v24.errors.GoogleAdsException; import com.google.ads.googleads.v24.resources.CampaignBudget; -import com.google.ads.googleads.v24.common.Metrics; import com.google.ads.googleads.v24.services.CampaignBudgetMapping; import com.google.ads.googleads.v24.services.CampaignBudgetOperation; import com.google.ads.googleads.v24.services.CampaignBudgetServiceClient; @@ -19,6 +19,7 @@ import com.google.ads.googleads.v24.services.MutateCampaignBudgetsResponse; import com.google.ads.googleads.v24.services.SearchGoogleAdsRequest; import com.google.api.gax.longrunning.OperationFuture; +import com.google.common.collect.Iterables; import com.google.protobuf.Empty; import java.io.FileNotFoundException; import java.io.IOException; @@ -26,15 +27,16 @@ import java.util.UUID; /** - * This example illustrates how to retrieve performance metrics for an experiment. + * Retrieves performance metrics for an experiment, evaluates the performance and takes action on + * the experiment accordingly. * - *

It shows how to query statistical significance metrics for the experiment, - * and how to execute actions such as promoting, ending, or graduating an experiment. + *

It shows how to query statistical significance metrics for the experiment, and how to execute + * actions such as promoting, ending, or graduating an experiment. */ -public class GetExperimentPerformance { +public class EvaluateAndUpdateExperiment { private static final double P_VALUE_THRESHOLD = 0.05; - private static class GetExperimentPerformanceParams extends CodeSampleParams { + private static class EvaluateAndUpdateExperimentParams extends CodeSampleParams { @Parameter(names = ArgumentNames.CUSTOMER_ID, required = true) private Long customerId; @@ -44,7 +46,7 @@ private static class GetExperimentPerformanceParams extends CodeSampleParams { } public static void main(String[] args) { - GetExperimentPerformanceParams params = new GetExperimentPerformanceParams(); + EvaluateAndUpdateExperimentParams params = new EvaluateAndUpdateExperimentParams(); if (!params.parseArguments(args)) { throw new IllegalArgumentException("Invalid or missing command line arguments"); } @@ -62,7 +64,8 @@ public static void main(String[] args) { } try { - new GetExperimentPerformance().runExample(googleAdsClient, params.customerId, params.experimentId); + new EvaluateAndUpdateExperiment() + .runExample(googleAdsClient, params.customerId, params.experimentId); } catch (GoogleAdsException gae) { System.err.printf( "Request ID %s failed due to GoogleAdsException. Underlying errors:%n", @@ -86,6 +89,9 @@ private void runExample(GoogleAdsClient googleAdsClient, long customerId, long e try (GoogleAdsServiceClient googleAdsServiceClient = googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) { + // Query to retrieve the experiment. + // Notice that we request the statistical metrics (for example, p-value, point estimate, + // margin of error) which are populated based on the treatment arm. String query = String.format( "SELECT " @@ -111,26 +117,39 @@ private void runExample(GoogleAdsClient googleAdsClient, long customerId, long e GoogleAdsServiceClient.SearchPagedResponse response = googleAdsServiceClient.search(request); - boolean experimentFound = false; - for (GoogleAdsRow row : response.iterateAll()) { - experimentFound = true; - System.out.printf("Found experiment: %s%n", row.getExperiment().getName()); - System.out.printf(" Resource Name: %s%n", row.getExperiment().getResourceName()); - - evaluateExperiment(googleAdsClient, customerId, row); - } - - if (!experimentFound) { + Iterable rows = response.iterateAll(); + if (Iterables.isEmpty(rows)) { System.out.printf("No experiment found for experiment ID: %d%n", experimentId); + return; } + GoogleAdsRow row = Iterables.getOnlyElement(rows); + System.out.printf("Found experiment: %s%n", row.getExperiment().getName()); + System.out.printf(" Resource Name: %s%n", row.getExperiment().getResourceName()); + + evaluateExperiment(googleAdsClient, customerId, row); } } - // [START get_experiment_performance_1] - private void evaluateExperiment(GoogleAdsClient googleAdsClient, long customerId, GoogleAdsRow row) { + /** + * Evaluates the performance of the experiment and updates it accordingly (for example, promotes, + * ends, or graduates). + * + *

Checks conversion and click metrics against statistical significance thresholds to determine + * the appropriate action to take on the experiment. + */ + // [START evaluate_and_update_experiment_1] + private void evaluateExperiment( + GoogleAdsClient googleAdsClient, long customerId, GoogleAdsRow row) { Metrics metrics = row.getMetrics(); String experimentResourceName = row.getExperiment().getResourceName(); + // 1. Evaluate conversion success as a primary success signal if available. + // - Point Estimate: Represents the estimated average lift or difference in conversions. + // - Margin of Error: Outlines the confidence interval bounds. Note that the margin_of_error + // provided by the API is calculated for a preset confidence level which is set based on the + // experiment type. + // - Lower Bound: (Point Estimate - Margin of Error). If this value is above 0, + // we have statistical significance that performance has improved. double convPValue = metrics.getConversionsAbsoluteChangePValue(); double convLift = metrics.getConversionsAbsoluteChangePointEstimate(); double convError = metrics.getConversionsAbsoluteChangeMarginOfError(); @@ -139,7 +158,8 @@ private void evaluateExperiment(GoogleAdsClient googleAdsClient, long customerId if (convPValue <= P_VALUE_THRESHOLD) { if (convLowerBound > 0) { System.out.printf( - "Significant Success: Conversions increased. Even at the lower bound, the lift is %.2f. Promoting changes.%n", + "Significant Success: Conversions increased. Even at the lower bound, the lift is %.2f." + + " Promoting changes.%n", convLowerBound); promoteExperiment(googleAdsClient, customerId, experimentResourceName); return; @@ -152,49 +172,77 @@ private void evaluateExperiment(GoogleAdsClient googleAdsClient, long customerId } } + // 2. Fall back to evaluating click metrics if conversions are inconclusive. double clickPValue = metrics.getClicksPValue(); double clickLift = metrics.getClicksPointEstimate(); double clickError = metrics.getClicksMarginOfError(); double clickLowerBound = clickLift - clickError; if (clickPValue <= P_VALUE_THRESHOLD && clickLowerBound > 0) { - System.out.printf( - "Click volume is significantly up (+%.1f%%). Graduating treatment for further manual analysis.%n", - clickLift * 100); + System.out.printf("Click volume is significantly up (+%.1f%%).%n", clickLift * 100); + // Graduation is only supported for separate campaign experiments, not + // intra-campaign experiments where there is no separate treatment campaign. ExperimentType experimentType = row.getExperiment().getType(); if (experimentType != ExperimentType.ADOPT_BROAD_MATCH_KEYWORDS && experimentType != ExperimentType.ADOPT_AI_MAX) { + System.out.println("Graduating treatment campaign for further manual analysis."); graduateExperiment(googleAdsClient, customerId, experimentResourceName); } else { System.out.println( - "Intra-campaign trial detected: Graduation is not supported because there is only one campaign. Continuing to run to gather more conversion data."); + "Intra-campaign trial detected: graduation is not supported. Continuing to run the" + + " experiment to gather more conversion data."); } } else { + // 3. Print status if no action was taken. System.out.printf( - "Inconclusive: No significant lift in Conversions (p=%.2f) or Clicks (p=%.2f). Current estimated lift: %.2f +/- %.2f. Continue running.%n", + "Inconclusive: No significant lift in Conversions (p=%.2f) or Clicks (p=%.2f). Current" + + " estimated lift: %.2f +/- %.2f. Allowing the experiment to continue running.%n", convPValue, clickPValue, convLift, convError); } } - // [END get_experiment_performance_1] - private void promoteExperiment(GoogleAdsClient googleAdsClient, long customerId, String experimentResourceName) { + // [END evaluate_and_update_experiment_1] + + /** + * Promotes the experiment trial campaign to the base campaign. + * + *

Promotion is an asynchronous long-running process that copies the trial campaign's settings + * and creatives back to the base campaign and subsequently ends the experiment. + */ + private void promoteExperiment( + GoogleAdsClient googleAdsClient, long customerId, String experimentResourceName) { try (ExperimentServiceClient experimentServiceClient = googleAdsClient.getLatestVersion().createExperimentServiceClient()) { + // This method returns a long running operation (LRO). + // - To block until the operation is complete: call operationResponse.get() + // - For non-blocking status checks: use operationResponse.isDone() + // - For manual polling or persistent tracking: store operationResponse.getName() + // + // For more information on handling LROs, see: + // https://developers.google.com/google-ads/api/docs/concepts/long-running-operations OperationFuture operationResponse = experimentServiceClient.promoteExperimentAsync(experimentResourceName); System.out.printf("Started promotion for experiment: %s%n", experimentResourceName); System.out.printf( - "The promotion is running asynchronously. You can track its progress using the long-running operation: %s%n", + "The promotion is running asynchronously. You can track its progress using the" + + " long-running operation: %s%n", operationResponse.getName()); System.out.println( - "Best Practice: If the promotion fails, you can retrieve the full list of errors by calling ExperimentService.ListExperimentAsyncErrors."); + "Best Practice: If the promotion fails, you can retrieve the full list of errors by" + + " calling ExperimentService.ListExperimentAsyncErrors."); } catch (Exception e) { System.out.printf("Failed to promote experiment: %s%n", e.getMessage()); } } - private void endExperiment(GoogleAdsClient googleAdsClient, long customerId, String experimentResourceName) { + /** + * Immediately ends the experiment. + * + *

Terminates the traffic split and sets the end date to the current time. + */ + private void endExperiment( + GoogleAdsClient googleAdsClient, long customerId, String experimentResourceName) { try (ExperimentServiceClient experimentServiceClient = googleAdsClient.getLatestVersion().createExperimentServiceClient()) { experimentServiceClient.endExperiment(experimentResourceName); @@ -202,10 +250,17 @@ private void endExperiment(GoogleAdsClient googleAdsClient, long customerId, Str } } - private void graduateExperiment(GoogleAdsClient googleAdsClient, long customerId, String experimentResourceName) { + /** + * Graduates the experiment to a full standalone campaign. + * + *

This process involves creating a new budget and mapping the treatment campaign to it. + */ + private void graduateExperiment( + GoogleAdsClient googleAdsClient, long customerId, String experimentResourceName) { String budgetResourceName; try (CampaignBudgetServiceClient campaignBudgetServiceClient = googleAdsClient.getLatestVersion().createCampaignBudgetServiceClient()) { + // 1. Create a new campaign budget for the graduating campaign. CampaignBudget campaignBudget = CampaignBudget.newBuilder() .setName("Graduated Experiment Budget #" + UUID.randomUUID()) @@ -227,14 +282,18 @@ private void graduateExperiment(GoogleAdsClient googleAdsClient, long customerId String treatmentCampaignResourceName = null; try (GoogleAdsServiceClient googleAdsServiceClient = googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) { + // 2. Query the experiment_arm to retrieve the treatment campaign's resource name. + // The treatment arm has control set to FALSE. String query = String.format( - "SELECT experiment_arm.campaigns FROM experiment_arm WHERE experiment_arm.experiment = '%s' AND experiment_arm.control = FALSE", + "SELECT experiment_arm.campaigns FROM experiment_arm WHERE experiment_arm.experiment" + + " = '%s' AND experiment_arm.control = FALSE", experimentResourceName); GoogleAdsServiceClient.SearchPagedResponse searchResponse = googleAdsServiceClient.search(Long.toString(customerId), query); + // Find the resource name of the treatment campaign. for (GoogleAdsRow row : searchResponse.iterateAll()) { if (row.getExperimentArm().getCampaignsCount() > 0) { treatmentCampaignResourceName = row.getExperimentArm().getCampaigns(0); @@ -243,6 +302,7 @@ private void graduateExperiment(GoogleAdsClient googleAdsClient, long customerId } } + // Verify that a treatment campaign was found. if (treatmentCampaignResourceName == null) { System.out.println("Could not find the treatment campaign associated with this experiment."); return; @@ -250,6 +310,7 @@ private void graduateExperiment(GoogleAdsClient googleAdsClient, long customerId try (ExperimentServiceClient experimentServiceClient = googleAdsClient.getLatestVersion().createExperimentServiceClient()) { + // 3. Build the budget mapping and execute the graduation request. CampaignBudgetMapping budgetMapping = CampaignBudgetMapping.newBuilder() .setExperimentCampaign(treatmentCampaignResourceName) From e4d0986a4924e71a03e729d7f3ca6b6d3ea29066 Mon Sep 17 00:00:00 2001 From: Sarah Date: Fri, 5 Jun 2026 16:35:25 -0400 Subject: [PATCH 4/4] Fix formatting --- .../examples/experiments/CreateSearchCustomExperiment.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java index 7dd0fb1b7..a1c08a8f8 100644 --- a/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java +++ b/google-ads-examples/src/main/java/com/google/ads/googleads/examples/experiments/CreateSearchCustomExperiment.java @@ -196,8 +196,7 @@ private String createExperimentArms( .setCustomerId(Long.toString(customerId)) .addAllOperations(operations) // We want to fetch the draft campaign IDs from the treatment arm, so the easiest way - // to do - // that is to have the response return the newly created entities. + // to do that is to have the response return the newly created entities. .setResponseContentType(ResponseContentType.MUTABLE_RESOURCE) .build();