Google Ads

Campaign Drafts and Experiments  |  AdWords API  |  Google Developers

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:

  1. Create a
    Draft
    from an existing base campaign. A draft is a mirror of an existing campaign
    that does not serve ads.
  2. Modify the draft campaign to suit the needs of your experiment.
  3. 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.
  4. 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:

  1. Promote the changes in the draft campaign
    directly to the base campaign.
  2. 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 %dn",
        $trial->getId()
    );
} else {
    printf("Could not create trial due to the following errors:n");
    $i = 0;
    foreach ($errors as $error) {
        printf("Error #%d: %sn", $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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to top button
Close