Tuesday, December 10, 2013

The Issues of Migrating from Bing Ads API V8 to V9

When I upgraded from Bing Ads API V8 to V9, a couple of issues popped up, and I tried out following workarounds to resolve them, please check out.
1. Partial Success for Ads and Keywords: When adding, updating, or deleting ads or keywords in batches of one or more, the operation may succeed for some and fail for part of the batch.
Here I tried to add 4 keywords, 2 of them were invalid. So the response has keyword ids as below, that’s corrent, 2 of them should not have id. But the problem happened when Axis tried to convert this id list to long[].  Please see the exception below. any idea to fix this issue?

                  <KeywordIds xmlns:a="http://schemas.datacontract.org/2004/07/System" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                        <a:long>15126706305</a:long>
                        <a:long i:nil="true" />
                        <a:long>15126706306</a:long>
                        <a:long i:nil="true" />
                  </KeywordIds>

ERROR [pool-8-thread-1] (BeanPropertyTarget.java:135) - Could not convert org.apache.axis.encoding.ser.ArrayDeserializer$ArrayListExtension to bean field 'keywordIds', type [J
ERROR [pool-8-thread-1] (Call.java:2469) - Exception:
java.lang.IllegalArgumentException
      at org.apache.axis.encoding.ser.BeanPropertyTarget.set(BeanPropertyTarget.java:157)
      at org.apache.axis.encoding.DeserializerImpl.valueComplete(DeserializerImpl.java:249)

Changing from long[] to Long[] will fix this problem

2. Bulk download campaign data or performance data potentially has extra Error row for each of  BulkDownloadEntity as documented here. which means the first column Type should precisely has following set of values, looks ugly! I think the error should be combined to it's above entity row.

<xs:simpleType name="BulkDownloadEntity">
  <xs:list>
    <xs:simpleType>
      <xs:restriction base="xs:string">
        <xs:enumeration value="Campaigns" />
        <xs:enumeration value="AdGroups" />
        <xs:enumeration value="Ads" />
        <xs:enumeration value="Keywords" />
        <xs:enumeration value="CampaignNegativeKeywords" />
        <xs:enumeration value="AdGroupNegativeKeywords" />
        <xs:enumeration value="CampaignTargets" />
        <xs:enumeration value="AdGroupTargets" />
        <xs:enumeration value="CampaignNegativeSites" />
        <xs:enumeration value="AdGroupNegativeSites" />
        <xs:enumeration value="CampaignSiteLinksAdExtensions" />
        <xs:enumeration value="CampaignProductAdExtensions" />
        <xs:enumeration value="CampaignLocationAdExtensions" />
        <xs:enumeration value="CampaignCallAdExtensions" />
        <xs:enumeration value="AdGroupProductTargets" />
        <xs:enumeration value="AdGroupSiteLinksAdExtensions" />
        <xs:enumeration value="CampaignsError" />
        <xs:enumeration value="AdGroupsError" />
        <xs:enumeration value="AdsError" />
        <xs:enumeration value="KeywordsError" />
        <xs:enumeration value="CampaignNegativeKeywordsError" />
        <xs:enumeration value="AdGroupNegativeKeywordsError" />
        <xs:enumeration value="CampaignTargetsError" />
        <xs:enumeration value="AdGroupTargetsError" />
        <xs:enumeration value="CampaignNegativeSitesError" />
        <xs:enumeration value="AdGroupNegativeSitesError" />
        <xs:enumeration value="CampaignSiteLinksAdExtensionsError" />
        <xs:enumeration value="CampaignProductAdExtensionsError" />
        <xs:enumeration value="CampaignLocationAdExtensionsError" />
        <xs:enumeration value="CampaignCallAdExtensionsError" />
        <xs:enumeration value="AdGroupProductTargetsError" />
        <xs:enumeration value="AdGroupSiteLinksAdExtensionsError" />
      </xs:restriction>
    </xs:simpleType>
  </xs:list>
</xs:simpleType>

So the campaign data parser has to deal with the potential Error rows, take the Keyword for example:
 @Override
 public List<BiddableKeyword> getBiddableKeywords(int batchSize) throws IOException {
  List<BiddableKeyword> keywords = new ArrayList<BiddableKeyword>();
  if (batchSize > 0 && !passedTypes.contains(TYPE_KEYWORD)) {
   boolean foundType = false;
   BiddableKeyword keyword = null;
   while (currentRow != null) {
    String rowType = getRowType(currentRow);
    if (rowType.equals(TYPE_KEYWORD)) {
     foundType = true;
     if (keywords.size() == batchSize)
      break; // check size first
     keyword = getBiddableKeyword(currentRow);
     keywords.add(keyword);
    } else if (rowType.equals(TYPE_KEYWORD + ERROR_SUFFIX)) {
     // handle the separate keyword error row!!!
     Long keywordApiId = Long.valueOf(getCellValue(currentRow, COLUMN_ID));
     if (keyword.getApiId().equals(keywordApiId)) {
      keyword.setDisapprovalReasons("Reason code: " + getCellValue(currentRow, COLUMN_EDITORIAL_REASON_CODE));
     }
    } else if (foundType) {
     passedTypes.add(TYPE_KEYWORD);
     break;
    }
    currentRow = reader.readNext();
   }
  }
  return keywords;
 }

 private BiddableKeyword getBiddableKeyword(String[] dataRow) {
  BiddableKeyword keyword = new BiddableKeyword();
  keyword.setSearchEngine(SearchEngine.BING);
  keyword.setUserStatus(getCellValue(dataRow, COLUMN_STATUS).toUpperCase());
  keyword.setApiId(getCellValue(dataRow, COLUMN_ID));
  keyword.getAdGroup(true).setApiId(getCellValue(dataRow, COLUMN_PARENT_ID));
  keyword.getAdGroup().getCampaign(true).setCampaignName(getCellValue(dataRow, COLUMN_CAMPAIGN));
  keyword.getAdGroup().setAdGroupName(getCellValue(dataRow, COLUMN_AD_GROUP));
  keyword.setKeywordText(getCellValue(dataRow, COLUMN_KEYWORD));
  if (keyword.getUserStatus() != UserStatus.DELETED) {
   String matchType = getCellValue(dataRow, COLUMN_MATCH_TYPE);
   keyword.setMatchType(matchType == null ? null : matchType.toUpperCase());
   keyword.setMaxCpc(getCellValue(dataRow, COLUMN_BID));
   keyword.setDestinationUrl(getCellValue(dataRow, COLUMN_DESTINATION_URL));
   keyword.setAdParam1(getCellValue(dataRow, COLUMN_PARAM1));
   keyword.setAdParam2(getCellValue(dataRow, COLUMN_PARAM2));
   keyword.setAdParam3(getCellValue(dataRow, COLUMN_PARAM3));
   keyword.setApprovalStatus(getCellValue(dataRow, COLUMN_EDITORIAL_STATUS));
   keyword.setDisapprovalReasons(getCellValue(dataRow, COLUMN_EDITORIAL_REASON_CODE));
   keyword.setQualityScore(getCellValue(dataRow, COLUMN_QUALITY_SCORE));
   keyword.identifyBMM(); // identify BMM match type!
  }
  return keyword;
 }

3. One more issue I found is also for the bulk download, when you deleted some negative keywords, the downloaded report will show these keywords as  Deleted, BUT  the Keyword column is empty, and I don’t know which one was deleted since the negative keyword has no Id, this is not helpful for the merge,

So to merge negative keywords, I have to treat them as a whole set, means you get all negative keywords for some campaigns or ad groups, and fully merge to db. the parser to get negative keywords looks like this:

 @Override
 public List<NegativeKeyword> getCampaignNegativeKeywords(int batchSize) throws IOException {
  List<NegativeKeyword> negativeKeywords = new ArrayList<NegativeKeyword>();
  if (batchSize > 0 && !passedTypes.contains(TYPE_CAMPAIGN_NEGATIVE_KEYWORD)) {
   boolean foundType = false;
   while (currentRow != null) {
    String rowType = getRowType(currentRow);
    if (rowType.equals(TYPE_CAMPAIGN_NEGATIVE_KEYWORD)) {
     foundType = true;
     if (negativeKeywords.size() >= batchSize)
      break; // check size first!
     long campaignId = Long.valueOf(getCellValue(currentRow, COLUMN_PARENT_ID));
     negativeKeywords.addAll(getCampaignNegativeKeywords(campaignId));
     continue;
    } else if (rowType.equals(TYPE_CAMPAIGN_NEGATIVE_KEYWORD + ERROR_SUFFIX)) {
     // skip error line!     
    } else if (foundType) {
     passedTypes.add(TYPE_CAMPAIGN_NEGATIVE_KEYWORD);
     break;
    }
    currentRow = reader.readNext();
   }
  }
  return negativeKeywords;
 }

 // just for this single campaign!
 private List<NegativeKeyword> getCampaignNegativeKeywords(long campaignId) throws IOException {
  List<NegativeKeyword> negativeKeywords = new ArrayList<NegativeKeyword>();
  if (campaignId > 0) {
   while (currentRow != null) {
    String rowType = getRowType(currentRow);
    if (rowType.equals(TYPE_CAMPAIGN_NEGATIVE_KEYWORD)) {
     NegativeKeyword keyword = getCampaignNegativeKeyword(currentRow);
     if (campaignId == keyword.getAdGroup().getCampaign().getApiId()) {
      if (!(keyword.getUserStatus() == UserStatus.DELETED && keyword.getKeywordText() == null)) // The delete one doesn't have keyword text, NOT helpful for merge!
       negativeKeywords.add(keyword);
     } else {
      break;
     }
    } else if (rowType.equals(TYPE_CAMPAIGN_NEGATIVE_KEYWORD + ERROR_SUFFIX)) {
     // skip error line!
    } else {
     break;
    }
    currentRow = reader.readNext();
   }
  }
  return negativeKeywords;
 }

 private NegativeKeyword getCampaignNegativeKeyword(String[] dataRow) {
  NegativeKeyword negativeKeyword = new NegativeKeyword();
  negativeKeyword.setMapLevel(MapLevel.CAMPAIGN);
  negativeKeyword.setUserStatus(getCellValue(dataRow, COLUMN_STATUS).toUpperCase());
  negativeKeyword.getAdGroup(true).getCampaign(true).setApiId(getCellValue(dataRow, COLUMN_PARENT_ID));
  negativeKeyword.getAdGroup().getCampaign().setCampaignName(getCellValue(dataRow, COLUMN_CAMPAIGN));
  negativeKeyword.setKeywordName(SearchEngine.BING, getCellValue(dataRow, COLUMN_KEYWORD));
  return negativeKeyword;
 }