Have you ever wondered, "If I changed the landing page of my ads, would I get more conversions?" or "What if I reworded my ad this way? Would that help drive traffic?" Setting up a test to isolate that kind of specific variable could be tedious, but using Campaign Drafts and Experiments, all the heavy lifting (copying data, setup, etc.) is done for you.
Using the DraftService and TrialService, setting up new campaign experiments is quick and easy. Here are the steps you'll need to follow to set it up:
- Create a Draft from an existing base campaign. A draft is a mirror of an existing campaign that does not serve ads.
- Modify the draft campaign to suit the needs of your experiment.
- Create a Trial from your Draft. The Trial provides access to a new trial campaign that begins as a copy of the draft campaign and splits traffic between the trial campaign and the base campaign. These trial campaigns are also known as experiments.
- Compare the statistics of your trial campaign to your base campaign to see which performs better for you.
These steps represent the most common workflow, but DraftService and TrialService are flexible and can be used in other ways. For example, you can use a draft to stage changes to a base campaign, then integrate the changes back into the base campaign without ever using a trial. Or, if you do use a trial and like the performance, you have the option of promoting its attributes back to the base campaign or graduating it into its own full-fledged campaign alongside the base campaign.
This flowchart shows the workflows you could employ using campaign
drafts and experiments:
Drafts
A Draft is an object that maintains the association between a draft campaign and
a base campaign. You create a Draft through DraftService by providing the
ID
of an existing campaign to serve as a base. DraftService automatically
creates a new draft campaign and associates it with the Draft. A Draft object is
not itself the draft campaign; it simply relates a draft campaign to its base
campaign. The draft campaign exists as an actual Campaign object. The draft
campaign's campaignId
is available as the Draft's draftCampaignId
. You can
modify the draft campaign like you would a real campaign, such as changing its
criteria, ad groups, bids, and ads. However, a draft campaign doesn't serve ads.
For an existing campaign to serve as a base for a Draft, it must meet certain requirements. It must be a Search, Search with Display Select campaign, or Display campaign (except for Mobile app campaign for the Display network), and it must have a non-shared budget (isExplicitlyShared on the campaign's budget must be false). Although experiments support most features of campaigns, there are a few exceptions.
Creating a Draft
To create a Draft, set a baseCampaignId
and give the Draft a
name. Note that the name must be unique among all Drafts within your account.
Here is an example:
Java
// Get the DraftService. DraftServiceInterface draftService = adWordsServices.get(session, DraftServiceInterface.class); Draft draft = new Draft(); draft.setBaseCampaignId(baseCampaignId); draft.setDraftName("Test Draft #" + System.currentTimeMillis()); DraftOperation draftOperation = new DraftOperation(); draftOperation.setOperator(Operator.ADD); draftOperation.setOperand(draft); draft = draftService.mutate(new DraftOperation[] {draftOperation}).getValue(0);
VB
Dim draft As New Draft() draft.baseCampaignId = baseCampaignId draft.draftName = "Test Draft #" + ExampleUtilities.GetRandomString() Dim draftOperation As New DraftOperation() draftOperation.operator = [Operator].ADD draftOperation.operand = draft
C#
Draft draft = new Draft() { baseCampaignId = baseCampaignId, draftName = "Test Draft #" + ExampleUtilities.GetRandomString() }; DraftOperation draftOperation = new DraftOperation() { @operator = Operator.ADD, operand = draft };
PHP
$draftService = $adWordsServices->get($session, DraftService::class); $operations = []; // Create a draft. $draft = new Draft(); $draft->setBaseCampaignId($baseCampaignId); $draft->setDraftname('Test Draft #' . uniqid()); // Create a draft operation and add it to the operations list. $operation = new DraftOperation(); $operation->setOperand($draft); $operation->setOperator(Operator::ADD); $operations[] = $operation; // Create the draft on the server and print out some information for // the created draft. $result = $draftService->mutate($operations); $draft = $result->getValue()[0];
Perl
my $draft = Google::Ads::AdWords::v201809::Draft->new({ baseCampaignId => $base_campaign_id, draftName => sprintf("Test Draft #%s", uniqid())}); # Create operation. my $draft_operation = Google::Ads::AdWords::v201809::DraftOperation->new({ operator => "ADD", operand => $draft });
Python
draft_service = client.GetService('DraftService', version='v201809') draft = { 'baseCampaignId': base_campaign_id, 'draftName': 'Test Draft #%s' % uuid.uuid4() } draft_operation = {'operator': 'ADD', 'operand': draft} draft = draft_service.mutate([draft_operation])['value'][0] draft_campaign_id = draft['draftCampaignId']
Ruby
draft_srv = adwords.service(:DraftService, API_VERSION) draft = { :base_campaign_id => base_campaign_id, :draft_name => 'Test Draft #%d' % (Time.new.to_f * 1000).to_i } draft_operation = {:operator => 'ADD', :operand => draft} draft_result = draft_srv.mutate([draft_operation]) draft = draft_result[:value].first draft_id = draft[:draft_id] draft_campaign_id = draft[:draft_campaign_id]
The Draft will include details like draftCampaignId
, draftId
, and
baseCampaignId
--all of which are important. draftCampaignId
is used to
modify the draft campaign and its ad groups, criteria, and ads. draftId
and
baseCampaignId
serve as reference for the Draft when creating a trial or
promoting the Draft.
Customizing the draft campaign
The draftCampaignId
field can be used as a real campaign ID, using any service
that might need such an ID (CampaignService, AdGroupService, CampaignCriterionService,
and so on). For example, to add a new language criterion to the campaign, simply
use the draftCampaignId
obtained from the Draft as your campaignId
:
Java
CampaignCriterionServiceInterface campaignCriterionService = adWordsServices.get(session, CampaignCriterionServiceInterface.class); Language language = new Language(); language.setId(1003L); // Spanish // Make sure to use the draftCampaignId when modifying the virtual draft campaign. CampaignCriterion campaignCriterion = new CampaignCriterion(); campaignCriterion.setCampaignId(draft.getDraftCampaignId()); campaignCriterion.setCriterion(language); CampaignCriterionOperation criterionOperation = new CampaignCriterionOperation(); criterionOperation.setOperator(Operator.ADD); criterionOperation.setOperand(campaignCriterion); campaignCriterion = campaignCriterionService .mutate(new CampaignCriterionOperation[] {criterionOperation}) .getValue(0);
VB
campaign_criterion_srv = adwords.service(:CampaignCriterionService, API_VERSION) criterion = { :xsi_type => 'Language', :id => 1003 # Spanish } criterion_operation = { # Make sure to use the draft_campaign_id when modifying the virtual draft # campaign. :operator => 'ADD', :operand => { :campaign_id => draft_campaign_id, :criterion => criterion } } criterion_result = campaign_criterion_srv.mutate([criterion_operation])
C#
Language language = new Language() { id = 1003L // Spanish }; // Make sure to use the draftCampaignId when modifying the virtual draft // campaign. CampaignCriterion campaignCriterion = new CampaignCriterion() { campaignId = draft.draftCampaignId, criterion = language }; CampaignCriterionOperation criterionOperation = new CampaignCriterionOperation() { @operator = Operator.ADD, operand = campaignCriterion }; campaignCriterion = campaignCriterionService.mutate( new CampaignCriterionOperation[] { criterionOperation }).value[0];
PHP
$campaignCriterionService = $adWordsServices->get($session, CampaignCriterionService::class); // Create a criterion. $language = new Language(); $language->setId(1003); // Spanish $campaignCriterion = new CampaignCriterion(); $campaignCriterion->setCampaignId($draft->getDraftCampaignId()); $campaignCriterion->setCriterion($language); // Create a campaign criterion operation and add it to the operations list. $operations = []; $operation = new CampaignCriterionOperation(); $operation->setOperand($campaignCriterion); $operation->setOperator(Operator::ADD); $operations[] = $operation; // Create a campaign criterion on the server. $campaignCriterionService->mutate($operations);
Perl
my $criterion = Google::Ads::AdWords::v201809::Language->new({ id => 1003 # Spanish }); my $operation = Google::Ads::AdWords::v201809::CampaignCriterionOperation->new({ operator => "ADD", operand => Google::Ads::AdWords::v201809::CampaignCriterion->new({ campaignId => $draft_campaign_id, criterion => $criterion })}); $result = $client->CampaignCriterionService()->mutate({operations => [$operation]}); $criterion = $result->get_value()->[0];
Python
campaign_criterion_service = client.GetService('CampaignCriterionService', version='v201809') criterion = { 'xsi_type': 'Language', 'id': 1003 # Spanish } criterion_operation = { # Make sure to use the draftCampaignId when modifying the virtual draft # campaign. 'operator': 'ADD', 'operand': { 'campaignId': draft_campaign_id, 'criterion': criterion } } criterion = campaign_criterion_service.mutate([criterion_operation])[ 'value'][0]
Ruby
campaign_criterion_srv = adwords.service(:CampaignCriterionService, API_VERSION) criterion = { :xsi_type => 'Language', :id => 1003 # Spanish } criterion_operation = { # Make sure to use the draft_campaign_id when modifying the virtual draft # campaign. :operator => 'ADD', :operand => { :campaign_id => draft_campaign_id, :criterion => criterion } } criterion_result = campaign_criterion_srv.mutate([criterion_operation])
Similar operations can be performed on ad groups within the draft campaign. For instance, you can fetch ad groups within a draft campaign using a filter:
Java
// Get the AdGroupService. AdGroupServiceInterface adGroupService = adWordsServices.get(session, AdGroupServiceInterface.class); // Create a selector that limits to ad groups in the draft campaign. Selector selector = new SelectorBuilder() .fields(AdGroupField.Id) .equals(AdGroupField.CampaignId, Long.toString(draftCampaignId)) .limit(100) .build(); // Make a 'get' request. AdGroupPage adGroupPage = adGroupService.get(selector); // Display the results. if (adGroupPage.getEntries() != null && adGroupPage.getEntries().length > 0) { System.out.printf( "Found %d of %d ad groups.%n", adGroupPage.getEntries().length, adGroupPage.getTotalNumEntries()); } else { System.out.println("No ad groups found."); }
VB
' Get the AdGroupService. Dim adGroupService As AdGroupService = CType(user.GetService( AdWordsService.v201809.AdGroupService), AdGroupService) ' Create a selector that limits to ad groups in the draft campaign. Dim selector As New Selector selector.fields = New String() { AdGroup.Fields.Id } selector.predicates = New Predicate() { Predicate.Equals(AdGroup.Fields.CampaignId, draftCampaignId) } selector.paging = Paging.Default Dim page As AdGroupPage = adGroupService.get(selector) ' Display the results. If ((Not page Is Nothing) AndAlso (Not page.entries Is Nothing)) Then Console.WriteLine("Fetched {0} of {1} ad groups.", page.entries.Length, page.totalNumEntries) End If
PHP
$adGroupService = $adWordsServices->get($session, AdGroupService::class); // Create a selector to select all ad groups for the specified draft // campaign. $selector = new Selector(); $selector->setFields(['Id']); $selector->setPredicates( [ new Predicate( 'CampaignId', PredicateOperator::EQUALS, [$draftCampaignId] ) ] ); $selector->setPaging(new Paging(0, self::PAGE_LIMIT)); // Retrieve ad groups for the specified draft campaign. $page = $adGroupService->get($selector); // Print out some information for the ad groups. if ($page->getTotalNumEntries() > 0) { printf("Found %d ad groups.\n", $page->getTotalNumEntries()); } else { print "No ad groups were found.\n"; }
Perl
# Create predicates. my $campaign_predicate = Google::Ads::AdWords::v201809::Predicate->new({ field => "CampaignId", operator => "EQUALS", values => [$draft_campaign_id]}); # Create selector. my $paging = Google::Ads::AdWords::v201809::Paging->new({ startIndex => 0, numberResults => PAGE_SIZE }); my $selector = Google::Ads::AdWords::v201809::Selector->new({ fields => ["Id"], predicates => [$campaign_predicate], paging => $paging }); my $ad_group_page = $client->AdGroupService()->get({ serviceSelector => $selector }); if ($ad_group_page->get_entries()) { printf( "Found %d of %d ad groups.\n", scalar(@{$ad_group_page->get_entries()}), $ad_group_page->get_totalNumEntries()); } else { printf("No ad groups found.\n"); }
Python
ad_group_service = client.GetService('AdGroupService', version='v201809') selector = { 'fields': ['Id'], 'paging': { 'startIndex': str(0), 'numberResults': str(PAGE_SIZE) }, 'predicates': [{ 'field': 'CampaignId', 'operator': 'IN', 'values': [draft_campaign_id] }] } response = ad_group_service.get(selector) draft_ad_groups = response['entries'] if 'entries' in response else []
Ruby
ad_group_srv = adwords.service(:AdGroupService, API_VERSION) selector = { :fields => ['Id'], :predicates => [{ :field => 'CampaignId', :operator => 'IN', :values => [draft_campaign_id] }], :paging => { :start_index => 0, :number_results => 100 } } ad_group_page = ad_group_srv.get(selector) unless ad_group_page[:entries].nil? puts "Found %d of %d ad groups." % [ad_group_page[:entries].size, ad_group_page[:total_num_entries]] else puts "No ad groups found." end
Policy checks for ads are done on draft campaigns just as they would be with normal campaigns. Policy violations that do not occur until an ad is serving will not result from added draft ads, but will result in an error if and when you run a trial off this draft.
Once you're done making changes to your draft campaign, you have two options:
- Promote the changes in the draft campaign directly to the base campaign.
- Create a trial campaign that will run concurrently with the base campaign.
Promoting the draft campaign
It's possible to use Drafts simply as a staging area for pending changes to your
real campaigns without ever creating a trial. When all of your changes are
staged in the draft campaign, you can simply promote the Draft, causing those
changes to be applied to the base campaign. To promote a Draft, submit a mutate
operation that sets its status to PROMOTING
. Since this involves copying all
changes from the draft campaign back to the base campaign, promotion is handled
asynchronously and is irreversible. Setting the status begins the asynchronous
process.
You can poll the Draft by using its draftId
and baseCampaignId
to monitor the
status field. When the status changes from PROMOTING
, the operation is complete.
PROMOTED
indicates success; PROMOTE_FAILED
indicates an error was encountered.
We discuss more about polling schemes in the Trials section.
If an error occurred while attempting to promote the draft, you can get more
detail on the specific errors by providing the draftId
and baseCampaignId
to DraftAsyncErrorService.
See the Errors section for an example.
Draft campaigns are identifiable because their campaignTrialType
is DRAFT
. You
can also look up the base campaign ID from a draft campaign by using the
baseCampaignId
field. For normal campaigns (those that were created neither from
a draft nor a trial), the campaignTrialType
is BASE
and the baseCampaignId
field
is the campaign's own ID.
Draft entities, by default, are not included in get-results from other services
such as CampaignService or AdGroupService. In order to fetch a draft entity,
such as a draft campaign or draft ad group, you need to either specify the
campaignTrialType
explicitly as DRAFT
in the predicates, or filter on an ID of a
known draft entity such as draftCampaignId
.
Trials
A Trial is an entity that helps manage a trial campaign running alongside a base campaign, taking a share of traffic and budget to test what you set up in your Draft. A Trial produces a real trial campaign (also called an experiment) which serves ads like a normal campaign. It collects statistics separately from your base campaign; however, a Trial still counts towards your account limits on the number of campaigns, ad groups, etc. you can have at one time.
Creating a Trial
To create a Trial, you provide the draftId
and baseCampaignId
to uniquely
identify the Draft from which you're starting the Trial, a unique name,
and the percentage of traffic you want going to the Trial. Creating a Trial
automatically creates a newly associated trial campaign.
Trials are uniquely identified by their ID, unlike Drafts, so once a Trial is
created, you don't need to use the baseCampaignId
to reference the Trial.
Java
// Get the TrialService. TrialServiceInterface trialService = adWordsServices.get(session, TrialServiceInterface.class); Trial trial = new Trial(); trial.setDraftId(draftId); trial.setBaseCampaignId(baseCampaignId); trial.setName("Test Trial #" + System.currentTimeMillis()); trial.setTrafficSplitPercent(50); trial.setTrafficSplitType(CampaignTrialTrafficSplitType.RANDOM_QUERY); TrialOperation trialOperation = new TrialOperation(); trialOperation.setOperator(Operator.ADD); trialOperation.setOperand(trial); long trialId = trialService.mutate(new TrialOperation[] {trialOperation}).getValue(0).getId();
VB
Dim newTrial As New Trial newTrial.draftId = draftId newTrial.baseCampaignId = baseCampaignId newTrial.name = "Test Trial #" & ExampleUtilities.GetRandomString() newTrial.trafficSplitPercent = 50 newTrial.trafficSplitType = CampaignTrialTrafficSplitType.RANDOM_QUERY Dim trialOperation As New TrialOperation() trialOperation.operator = [Operator].ADD trialOperation.operand = newTrial
C#
Trial trial = new Trial() { draftId = draftId, baseCampaignId = baseCampaignId, name = "Test Trial #" + ExampleUtilities.GetRandomString(), trafficSplitPercent = 50, trafficSplitType = CampaignTrialTrafficSplitType.RANDOM_QUERY }; TrialOperation trialOperation = new TrialOperation() { @operator = Operator.ADD, operand = trial };
PHP
$trialService = $adWordsServices->get($session, TrialService::class); $trialAsynErrorService = $adWordsServices->get($session, TrialAsyncErrorService::class); // Create a trial. $trial = new Trial(); $trial->setDraftId($draftId); $trial->setBaseCampaignId($baseCampaignId); $trial->setName('Test Trial #' . uniqid()); $trial->setTrafficSplitPercent(50); $trial->setTrafficSplitType( CampaignTrialTrafficSplitType::RANDOM_QUERY ); // Create a trial operation and add it to the operations list. $operations = []; $operation = new TrialOperation(); $operation->setOperand($trial); $operation->setOperator(Operator::ADD); $operations[] = $operation; // Create the trial on the server. $trial = $trialService->mutate($operations)->getValue()[0];
Perl
my $trial = Google::Ads::AdWords::v201809::Trial->new({ draftId => $draft_id, baseCampaignId => $base_campaign_id, name => sprintf("Test Trial #%s", uniqid()), trafficSplitPercent => 50, trafficSplitType => "RANDOM_QUERY" }); # Create operation. my $trial_operation = Google::Ads::AdWords::v201809::TrialOperation->new({ operator => "ADD", operand => $trial }); # Add trial. my $result = $client->TrialService()->mutate({operations => [$trial_operation]});
Python
trial_service = client.GetService('TrialService', version='v201809') trial_async_error_service = client.GetService('TrialAsyncErrorService', version='v201809') trial = { 'draftId': draft_id, 'baseCampaignId': base_campaign_id, 'name': 'Test Trial #%d' % uuid.uuid4(), 'trafficSplitPercent': 50, 'trafficSplitType': 'RANDOM_QUERY' } trial_operation = {'operator': 'ADD', 'operand': trial} trial_id = trial_service.mutate([trial_operation])['value'][0]['id']
Ruby
trial_srv = adwords.service(:TrialService, API_VERSION) trial_async_error_srv = adwords.service(:TrialAsyncErrorService, API_VERSION) trial = { :draft_id => draft_id, :base_campaign_id => base_campaign_id, :name => 'Test Trial #%d' % (Time.new.to_f * 1000).to_i, :traffic_split_percent => 50, :traffic_split_type => 'RANDOM_QUERY' } trial_operation = {:operator => 'ADD', :operand => trial} trial_result = trial_srv.mutate([trial_operation]) trial_id = trial_result[:value].first[:id]
As with Draft creation, take note of the Trial's trialCampaignId
for use
in future operations.
Creating a Trial is an asynchronous operation (unlike Draft creation). A new
Trial will have a status of CREATING
. From there, you should poll until it
is in some other status before proceeding:
Java
Selector trialSelector = new SelectorBuilder() .fields( TrialField.Id, TrialField.Status, TrialField.BaseCampaignId, TrialField.TrialCampaignId) .equalsId(trialId) .build(); trial = null; boolean isPending = true; int pollAttempts = 0; do { long sleepSeconds = (long) Math.scalb(30d, pollAttempts); System.out.printf("Sleeping for %d seconds.%n", sleepSeconds); Thread.sleep(sleepSeconds * 1000); trial = trialService.get(trialSelector).getEntries(0); System.out.printf("Trial ID %d has status '%s'.%n", trial.getId(), trial.getStatus()); pollAttempts++; isPending = TrialStatus.CREATING.equals(trial.getStatus()); } while (isPending && pollAttempts < MAX_POLL_ATTEMPTS);
VB
' Since creating a trial is asynchronous, we have to poll it to wait ' for it to finish. Dim trialSelector As New Selector() trialSelector.fields = New String() { _ Trial.Fields.Id, Trial.Fields.Status, Trial.Fields.BaseCampaignId, Trial.Fields.TrialCampaignId } trialSelector.predicates = New Predicate() { _ Predicate.Equals( Trial.Fields.Id, trialId) } newTrial = Nothing Dim isPending As Boolean = True Dim pollAttempts As Integer = 0 Do Dim sleepMillis As Integer = CType(Math.Pow(2, pollAttempts)* POLL_INTERVAL_SECONDS_BASE*1000, Integer) Console.WriteLine("Sleeping {0} millis...", sleepMillis) Thread.Sleep(sleepMillis) newTrial = trialService.get(trialSelector).entries(0) Console.WriteLine("Trial ID {0} has status '{1}'.", newTrial.id, newTrial.status) pollAttempts = pollAttempts + 1 isPending = (newTrial.status = TrialStatus.CREATING) Loop While isPending AndAlso (pollAttempts <= MAX_RETRIES) If newTrial.status = TrialStatus.ACTIVE Then ' The trial creation was successful. Console.WriteLine("Trial created with ID {0} and trial campaign " & "ID {1}.", newTrial.id, newTrial.trialCampaignId) ElseIf newTrial.status = TrialStatus.CREATION_FAILED Then ' The trial creation failed, and errors can be fetched from the ' TrialAsyncErrorService. Dim errorsSelector As New Selector() errorsSelector.fields = New String() { _ TrialAsyncError.Fields.TrialId, TrialAsyncError.Fields. AsyncError } errorsSelector.predicates = New Predicate() { _ Predicate.Equals(TrialAsyncError.Fields.TrialId, newTrial.id) } Dim trialAsyncErrorPage As TrialAsyncErrorPage = trialAsyncErrorService. get( errorsSelector) If trialAsyncErrorPage.entries Is Nothing OrElse trialAsyncErrorPage.entries.Length = 0 Then Console.WriteLine("Could not retrieve errors for trial {0}.", newTrial.id) Else Console.WriteLine( "Could not create trial ID {0} for draft ID {1} due to the " & "following errors:", trialId, draftId) Dim i As Integer = 1 For Each err As TrialAsyncError In trialAsyncErrorPage.entries Dim asyncError As ApiError = err.asyncError Console.WriteLine( "Error #{0}: errorType='{1}', errorString='{2}', " & "trigger='{3}', fieldPath='{4}'", i, asyncError.ApiErrorType, asyncError.errorString, asyncError.trigger, asyncError.fieldPath) i += 1 Next End If Else ' Most likely, the trial is still being created. You can continue ' polling, but we have limited the number of attempts in the ' example. Console.WriteLine( "Timed out waiting to create trial from draft ID {0} with " + "base campaign ID {1}.", draftId, baseCampaignId) End If
C#
// Since creating a trial is asynchronous, we have to poll it to wait // for it to finish. Selector trialSelector = new Selector() { fields = new string[] { Trial.Fields.Id, Trial.Fields.Status, Trial.Fields.BaseCampaignId, Trial.Fields.TrialCampaignId }, predicates = new Predicate[] { Predicate.Equals(Trial.Fields.Id, trialId) } }; trial = null; bool isPending = true; int pollAttempts = 0; do { int sleepMillis = (int) Math.Pow(2, pollAttempts) * POLL_INTERVAL_SECONDS_BASE * 1000; Console.WriteLine("Sleeping {0} millis...", sleepMillis); Thread.Sleep(sleepMillis); trial = trialService.get(trialSelector).entries[0]; Console.WriteLine("Trial ID {0} has status '{1}'.", trial.id, trial.status); pollAttempts++; isPending = (trial.status == TrialStatus.CREATING); } while (isPending && pollAttempts <= MAX_RETRIES); if (trial.status == TrialStatus.ACTIVE) { // The trial creation was successful. Console.WriteLine( "Trial created with ID {0} and trial campaign ID {1}.", trial.id, trial.trialCampaignId); } else if (trial.status == TrialStatus.CREATION_FAILED) { // The trial creation failed, and errors can be fetched from the // TrialAsyncErrorService. Selector errorsSelector = new Selector() { fields = new string[] { TrialAsyncError.Fields.TrialId, TrialAsyncError.Fields.AsyncError }, predicates = new Predicate[] { Predicate.Equals(TrialAsyncError.Fields.TrialId, trial.id) } }; TrialAsyncErrorPage trialAsyncErrorPage = trialAsyncErrorService.get(errorsSelector); if (trialAsyncErrorPage.entries == null || trialAsyncErrorPage.entries.Length == 0) { Console.WriteLine("Could not retrieve errors for trial {0}.", trial.id); } else { Console.WriteLine( "Could not create trial ID {0} for draft ID {1} due to the " + "following errors:", trial.id, draftId); int i = 0; foreach (TrialAsyncError error in trialAsyncErrorPage.entries) { ApiError asyncError = error.asyncError; Console.WriteLine( "Error #{0}: errorType='{1}', errorString='{2}', " + "trigger='{3}', fieldPath='{4}'", i++, asyncError.ApiErrorType, asyncError.errorString, asyncError.trigger, asyncError.fieldPath); } } } else { // Most likely, the trial is still being created. You can continue // polling, but we have limited the number of attempts in the // example. Console.WriteLine( "Timed out waiting to create trial from draft ID {0} with " + "base campaign ID {1}.", draftId, baseCampaignId); }
PHP
$selector = new Selector(); $selector->setFields( ['Id', 'Status', 'BaseCampaignId', 'TrialCampaignId'] ); $selector->setPredicates( [new Predicate('Id', PredicateOperator::IN, [$trial->getId()])] ); // Since creating a trial is asynchronous, we have to poll it to wait for it // to finish. $pollAttempts = 0; $isPending = true; $trial = null; do { $sleepSeconds = self::POLL_FREQUENCY_SECONDS * pow(2, $pollAttempts); printf("Sleeping %d seconds...\n", $sleepSeconds); sleep($sleepSeconds); $trial = $trialService->get($selector)->getEntries()[0]; printf( "Trial ID %d has status '%s'.\n", $trial->getId(), $trial->getStatus() ); $pollAttempts++; $isPending = ($trial->getStatus() === TrialStatus::CREATING) ? true : false; } while ($isPending && $pollAttempts <= self::MAX_POLL_ATTEMPTS);
Perl
my $trial_id = $result->get_value()->[0]->get_id()->get_value(); my $predicate = Google::Ads::AdWords::v201809::Predicate->new({ field => "Id", operator => "IN", values => [$trial_id]}); my $paging = Google::Ads::AdWords::v201809::Paging->new({ startIndex => 0, numberResults => 1 }); my $selector = Google::Ads::AdWords::v201809::Selector->new({ fields => ["Id", "Status", "BaseCampaignId", "TrialCampaignId"], predicates => [$predicate], paging => $paging }); # Since creating a trial is asynchronous, we have to poll it to wait for # it to finish. my $poll_attempts = 0; my $is_pending = 1; my $end_time = time + JOB_TIMEOUT_IN_MILLISECONDS; do { # Check to see if the trial is still in the process of being created. my $result = $client->TrialService()->get({selector => $selector}); $trial = $result->get_entries()->[0]; my $waittime_in_milliseconds = JOB_BASE_WAITTIME_IN_MILLISECONDS * (2**$poll_attempts); if (((time + $waittime_in_milliseconds) < $end_time) and $trial->get_status() eq 'CREATING') { printf("Sleeping %d milliseconds...\n", $waittime_in_milliseconds); sleep($waittime_in_milliseconds / 1000); # Convert to seconds. $poll_attempts++; } } while (time < $end_time and $trial->get_status() eq 'CREATING');
Python
selector = { 'fields': ['Id', 'Status', 'BaseCampaignId', 'TrialCampaignId'], 'predicates': [{ 'field': 'Id', 'operator': 'IN', 'values': [trial_id] }] } # Since creating a trial is asynchronous, we have to poll it to wait for it to # finish. poll_attempts = 0 is_pending = True trial = None while is_pending and poll_attempts < MAX_POLL_ATTEMPTS: trial = trial_service.get(selector)['entries'][0] print('Trial ID %d has status "%s"' % (trial['id'], trial['status'])) poll_attempts += 1 is_pending = trial['status'] == 'CREATING' if is_pending: sleep_seconds = 30 * (2 ** poll_attempts) print('Sleeping for %d seconds.' % sleep_seconds) time.sleep(sleep_seconds)
Ruby
selector = { :fields => ['Id', 'Status', 'BaseCampaignId', 'TrialCampaignId'], :predicates => [ :field => 'Id', :operator => 'IN', :values => [trial_id] ] } poll_attempts = 0 is_pending = true trial = nil begin sleep_seconds = 30 * (2 ** poll_attempts) puts "Sleeping for %d seconds" % sleep_seconds sleep(sleep_seconds) trial = trial_srv.get(selector)[:entries].first puts "Trial ID %d has status '%s'" % [trial[:id], trial[:status]] poll_attempts += 1 is_pending = (trial[:status] == 'CREATING') end while is_pending and poll_attempts < MAX_POLL_ATTEMPTS
Once you create a Trial, the associated trial campaign will act mostly as a normal campaign. You can tweak it as the trial is running, except for the following fields which are dictated by the Trial and are immutable in the trial campaign (unlike in a normal campaign):
status
name
startDate
endDate
budget
Trying to modify one of these fields on the trial campaign will result in a
CampaignError.CANNOT_MODIFY_FOR_TRIAL_CAMPAIGN
error. You can still change
most of these values by modifying them elsewhere and having them propagate down
to the trial campaign.
When creating a Trial, if unspecified, startDate
and endDate
will default to
the base campaign's dates. Modifications to startDate
, endDate
, and name
of the Trial will propagate to the trial campaign. Modifications to status
of
the base campaign will also propagate to the trial campaign. The budget is
shared with the base campaign and cannot be modified.
Trial operations
Trials have two separate asynchronous operations: creation and promotion. If an error occurred while attempting to create or promote a Trial, you can get more detail on the specific errors encountered by providing the Trial's ID to TrialAsyncErrorService. See the Errors section for an example.
You can also permanently halt a Trial immediately by placing it
into the HALTED
status. You cannot un-halt a Trial to get it to start serving
alongside the base campaign again, but you can promote, graduate, or
archive a halted Trial.
Trial campaigns always share a budget implicitly with their base campaign.
That budget cannot be shared with any other campaigns and must be specifically
marked as non-shared by setting isExplicitlyShared
to false.
During a trial, both auctions and budgets are shared between base and trial
campaigns, the ratio of which is determined by the traffic split percentage of
the trial. The trafficSplitPercent
field of the Trial
defines how much of the traffic goes to the trial, with the remainder going to
the base campaign. The trafficSplitType
(introduced in v201809 for search
campaigns only) defines how the traffic will be split between the base campaign
and the trial campaign. For trials created before v201809, this defaulted to
RANDOM_QUERY
. A traffic split type of RANDOM_QUERY
means that choosing the
base or trial campaign will be determined randomly at auction time, while
COOKIE
means that the system will use cookies to always present a given user
with one or the other, consistently. If one of the two exhausts its budget
early, the other continues to run and is only entered in its apportioned
percentage of auctions until it also exhausts its own budget or the trial ends.
After the trial ends, the base campaign resumes handling all auctions and gets
100% of the budget.
Trial campaigns are identifiable using their campaignTrialType
, which will be
TRIAL
. As with Drafts, their baseCampaignId
will point you at the base campaign
that the trial is copied from.
Promotion, graduation, and archiving
Once you've decided whether you like the results of your trial, you have a few options, and they're accomplished by putting the Trial into one of various statuses.
If you didn't like how the Trial performed, you can archive it by putting it into the
ARCHIVED
status. This will immediately mark the trial campaign as REMOVED
and
stop the experiment.
Alternatively, if you did like how the Trial performed and the trial is still
ACTIVE
, you can choose to implement the trial campaign more permanently in one
of two ways. You can either apply all its modifications back into the base
campaign, which is referred to as promotion. Or, you can allow the existing
trial campaign to exist independently of the Trial itself, allowing it to function
as a full campaign and allowing all normally immutable fields during a trial to
be modifiable again. This latter process is known as graduation.
Promotion is an asynchronous process, much like promoting a Draft. To start, put
the Trial into the PROMOTING
status and poll it until you see that it's either
PROMOTED
or PROMOTE_FAILED
. You can check for asynchronous errors with the
TrialAsyncErrorService.
Graduating is a synchronous process. Once you set the Trial to GRADUATED
, the
campaign is immediately ready to be modified and run. When graduating a trial
campaign, you must also specify a budgetId
for a new budget that the campaign
will use. It cannot continue to share the base campaign's budget after
graduation.
Reporting
The trial campaign does not copy previous statistics from its base campaign; it starts with a fresh slate. During the run of the trial, statistics for the base campaign and the trial campaign are accrued separately; each one has its own impressions, clicks, etc. This does not change when going through either promotion or graduation--statistics stay where they are and are never copied over to a different entity.
After promotion, the base campaign keeps all of its past stats and goes forward with the new changes copied into the base campaign. The stats from the trial campaign after promotion are still in the trial campaign.
After graduation, the base campaign and trial campaign continue to exist as separate entities and each one keeps its own stats for reporting.
You can use all the usual reports such as Campaign Performance Report for these
entities. The baseCampaignId
field on campaigns represents the base campaign, and
the campaignTrialType
field allows you to distinguish between regular and trial
campaigns.
Errors
Both Drafts and Trials allow certain kinds of actions (promotion for Drafts, and creation and promotion for Trials) that are asynchronous. In these cases, you should poll the service, exponentially backing off, until the operation either completes successfully or there is an error. If there is an error, the entity's status will indicate it, and you can then check the appropriate AsyncErrorService for details.
As an example, if a Trial fails to promote, you can request detailed errors like this:
Java
Selector errorsSelector = new SelectorBuilder() .fields(TrialAsyncErrorField.TrialId, TrialAsyncErrorField.AsyncError) .equals(TrialAsyncErrorField.TrialId, trial.getId().toString()) .build(); TrialAsyncErrorServiceInterface trialAsyncErrorService = adWordsServices.get(session, TrialAsyncErrorServiceInterface.class); TrialAsyncErrorPage trialAsyncErrorPage = trialAsyncErrorService.get(errorsSelector); if (trialAsyncErrorPage.getEntries() == null || trialAsyncErrorPage.getEntries().length == 0) { System.out.printf( "Could not retrieve errors for trial ID %d for draft ID %d.%n", trial.getId(), draftId); } else { System.out.printf( "Could not create trial ID %d for draft ID %d due to the following errors:%n", trial.getId(), draftId); int i = 0; for (TrialAsyncError error : trialAsyncErrorPage.getEntries()) { ApiError asyncError = error.getAsyncError(); System.out.printf( "Error #%d: errorType='%s', errorString='%s', trigger='%s', fieldPath='%s'%n", i++, asyncError.getApiErrorType(), asyncError.getErrorString(), asyncError.getTrigger(), asyncError.getFieldPath()); }
VB
ElseIf newTrial.status = TrialStatus.CREATION_FAILED Then ' The trial creation failed, and errors can be fetched from the ' TrialAsyncErrorService. Dim errorsSelector As New Selector() errorsSelector.fields = New String() { _ TrialAsyncError.Fields.TrialId, TrialAsyncError.Fields. AsyncError } errorsSelector.predicates = New Predicate() { _ Predicate.Equals(TrialAsyncError.Fields.TrialId, newTrial.id) } Dim trialAsyncErrorPage As TrialAsyncErrorPage = trialAsyncErrorService. get( errorsSelector) If trialAsyncErrorPage.entries Is Nothing OrElse trialAsyncErrorPage.entries.Length = 0 Then Console.WriteLine("Could not retrieve errors for trial {0}.", newTrial.id) Else Console.WriteLine( "Could not create trial ID {0} for draft ID {1} due to the " & "following errors:", trialId, draftId) Dim i As Integer = 1 For Each err As TrialAsyncError In trialAsyncErrorPage.entries Dim asyncError As ApiError = err.asyncError Console.WriteLine( "Error #{0}: errorType='{1}', errorString='{2}', " & "trigger='{3}', fieldPath='{4}'", i, asyncError.ApiErrorType, asyncError.errorString, asyncError.trigger, asyncError.fieldPath) i += 1 Next End If
C#
} else if (trial.status == TrialStatus.CREATION_FAILED) { // The trial creation failed, and errors can be fetched from the // TrialAsyncErrorService. Selector errorsSelector = new Selector() { fields = new string[] { TrialAsyncError.Fields.TrialId, TrialAsyncError.Fields.AsyncError }, predicates = new Predicate[] { Predicate.Equals(TrialAsyncError.Fields.TrialId, trial.id) } }; TrialAsyncErrorPage trialAsyncErrorPage = trialAsyncErrorService.get(errorsSelector); if (trialAsyncErrorPage.entries == null || trialAsyncErrorPage.entries.Length == 0) { Console.WriteLine("Could not retrieve errors for trial {0}.", trial.id); } else { Console.WriteLine( "Could not create trial ID {0} for draft ID {1} due to the " + "following errors:", trial.id, draftId); int i = 0; foreach (TrialAsyncError error in trialAsyncErrorPage.entries) { ApiError asyncError = error.asyncError; Console.WriteLine( "Error #{0}: errorType='{1}', errorString='{2}', " + "trigger='{3}', fieldPath='{4}'", i++, asyncError.ApiErrorType, asyncError.errorString, asyncError.trigger, asyncError.fieldPath); } }
PHP
$selector = new Selector(); $selector->setFields(['TrialId', 'AsyncError']); $selector->setPredicates( [new Predicate('TrialId', PredicateOperator::IN, [$trial->getId()])] ); $errors = $trialAsynErrorService->get($selector)->getEntries(); if (count($errors) === 0) { printf( "Could not retrieve errors for the trial with ID %d\n", $trial->getId() ); } else { printf("Could not create trial due to the following errors:\n"); $i = 0; foreach ($errors as $error) { printf("Error #%d: %s\n", $i++, $error->getAsyncError()); } }
Perl
my $error_selector = Google::Ads::AdWords::v201809::Selector->new({ fields => ["TrialId", "AsyncError"], predicates => [ Google::Ads::AdWords::v201809::Predicate->new({ field => "TrialId", operator => "IN", values => [$trial_id]})]}); my $errors = $client->TrialAsyncErrorService->get({selector => $error_selector}) ->get_entries(); if (!$errors) { printf("Could not retrieve errors for trial %d", $trial->get_id()); } else { printf("Could not create trial due to the following errors:"); my $index = 0; for my $error ($errors) { printf("Error %d: %s", $index, $error->get_asyncError() ->get_errorString()); $index++; } }
Python
selector = { 'fields': ['TrialId', 'AsyncError'], 'predicates': [{ 'field': 'TrialId', 'operator': 'IN', 'values': [trial['id']] }] } errors = trial_async_error_service.get(selector)['entries'] if not errors: print('Could not retrieve errors for trial %d' % trial['id']) else: print('Could not create trial due to the following errors:') for error in errors: print('Error: %s' % error['asyncError'])
Ruby
selector = { :fields => ['TrialId', 'AsyncError'], :predicates => [ {:field => 'TrialId', :operator => 'IN', :values => [trial[:id]]} ] } errors = trial_async_error_srv.get(selector)[:entries] if errors.nil? puts "Could not retrieve errors for trial %d" % trial[:id] else puts "Could not create trial due to the following errors:" errors.each_with_index do |error, i| puts "Error #%d: %s" % [i, error[:async_error]] end end
Asynchronous operations generally continue to apply their changes even after running into an error, so logged errors could grow rapidly if there are a significant number of incompatible modifications. Possible causes include duplicate ad group names, incompatible bidding strategies, or exceeding account limits.