Upload Enhanced Conversions for Web

Java

 // Copyright 2021 Google LLC 
 // 
 // Licensed under the Apache License, Version 2.0 (the "License"); 
 // you may not use this file except in compliance with the License. 
 // You may obtain a copy of the License at 
 // 
 //     https://www.apache.org/licenses/LICENSE-2.0 
 // 
 // Unless required by applicable law or agreed to in writing, software 
 // distributed under the License is distributed on an "AS IS" BASIS, 
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 // See the License for the specific language governing permissions and 
 // limitations under the License. 
 package 
  
 com.google.ads.googleads.examples.remarketing 
 ; 
 import 
  
 com.beust.jcommander.Parameter 
 ; 
 import 
  
 com.google.ads.googleads.examples.utils.ArgumentNames 
 ; 
 import 
  
 com.google.ads.googleads.examples.utils.CodeSampleParams 
 ; 
 import 
  
 com.google.ads.googleads.lib.GoogleAdsClient 
 ; 
 import 
  
 com.google.ads.googleads.v21.common.OfflineUserAddressInfo 
 ; 
 import 
  
 com.google.ads.googleads.v21.common.UserIdentifier 
 ; 
 import 
  
 com.google.ads.googleads.v21.enums.ConversionAdjustmentTypeEnum.ConversionAdjustmentType 
 ; 
 import 
  
 com.google.ads.googleads.v21.enums.UserIdentifierSourceEnum.UserIdentifierSource 
 ; 
 import 
  
 com.google.ads.googleads.v21.errors.GoogleAdsError 
 ; 
 import 
  
 com.google.ads.googleads.v21.errors.GoogleAdsException 
 ; 
 import 
  
 com.google.ads.googleads.v21.services.ConversionAdjustment 
 ; 
 import 
  
 com.google.ads.googleads.v21.services.ConversionAdjustmentResult 
 ; 
 import 
  
 com.google.ads.googleads.v21.services.ConversionAdjustmentUploadServiceClient 
 ; 
 import 
  
 com.google.ads.googleads.v21.services.GclidDateTimePair 
 ; 
 import 
  
 com.google.ads.googleads.v21.services.UploadConversionAdjustmentsRequest 
 ; 
 import 
  
 com.google.ads.googleads.v21.services.UploadConversionAdjustmentsResponse 
 ; 
 import 
  
 com.google.ads.googleads.v21.utils.ResourceNames 
 ; 
 import 
  
 com.google.common.collect.ImmutableMap 
 ; 
 import 
  
 java.io.FileNotFoundException 
 ; 
 import 
  
 java.io.IOException 
 ; 
 import 
  
 java.io.UnsupportedEncodingException 
 ; 
 import 
  
 java.security.MessageDigest 
 ; 
 import 
  
 java.security.NoSuchAlgorithmException 
 ; 
 import 
  
 java.util.ArrayList 
 ; 
 import 
  
 java.util.HashSet 
 ; 
 import 
  
 java.util.List 
 ; 
 import 
  
 java.util.Map 
 ; 
 import 
  
 java.util.Set 
 ; 
 /** 
 * Enhances a web conversion by uploading a {@link ConversionAdjustment} containing hashed user 
 * identifiers and an order ID. 
 */ 
 public 
  
 class 
 UploadEnhancedConversionsForWeb 
  
 { 
  
 private 
  
 static 
  
 class 
 UploadEnhancedConversionsForWebParams 
  
 extends 
  
 CodeSampleParams 
  
 { 
  
 @Parameter 
 ( 
 names 
  
 = 
  
 ArgumentNames 
 . 
 CUSTOMER_ID 
 , 
  
 required 
  
 = 
  
 true 
 ) 
  
 private 
  
 long 
  
 customerId 
 ; 
  
 @Parameter 
 ( 
 names 
  
 = 
  
 ArgumentNames 
 . 
 CONVERSION_ACTION_ID 
 , 
  
 required 
  
 = 
  
 true 
 ) 
  
 private 
  
 long 
  
 conversionActionId 
 ; 
  
 @Parameter 
 ( 
 names 
  
 = 
  
 ArgumentNames 
 . 
 ORDER_ID 
 , 
  
 required 
  
 = 
  
 true 
 ) 
  
 private 
  
 String 
  
 orderId 
 ; 
  
 @Parameter 
 ( 
  
 names 
  
 = 
  
 ArgumentNames 
 . 
 CONVERSION_DATE_TIME 
 , 
  
 required 
  
 = 
  
 false 
 , 
  
 description 
  
 = 
  
 "The date time at which the conversion with the specified order ID occurred. " 
  
 + 
  
 "Must be after the click time, and must include the time zone offset. " 
  
 + 
  
 "The format is  'yyyy-mm-dd hh:mm:ss+|-hh:mm', e.g. '2019-01-01 12:32:45-08:00'. " 
  
 + 
  
 "Setting this field is optional, but recommended." 
 ) 
  
 private 
  
 String 
  
 conversionDateTime 
 ; 
  
 @Parameter 
 ( 
 names 
  
 = 
  
 ArgumentNames 
 . 
 USER_AGENT 
 , 
  
 required 
  
 = 
  
 false 
 ) 
  
 private 
  
 String 
  
 userAgent 
 ; 
  
 } 
  
 public 
  
 static 
  
 void 
  
 main 
 ( 
 String 
 [] 
  
 args 
 ) 
  
 throws 
  
 UnsupportedEncodingException 
 , 
  
 NoSuchAlgorithmException 
  
 { 
  
 UploadEnhancedConversionsForWebParams 
  
 params 
  
 = 
  
 new 
  
 UploadEnhancedConversionsForWebParams 
 (); 
  
 if 
  
 ( 
 ! 
 params 
 . 
 parseArguments 
 ( 
 args 
 )) 
  
 { 
  
 // Either pass the required parameters for this example on the command line, or insert them 
  
 // into the code here. See the parameter class definition above for descriptions. 
  
 params 
 . 
 customerId 
  
 = 
  
 Long 
 . 
 parseLong 
 ( 
 "INSERT_CUSTOMER_ID_HERE" 
 ); 
  
 params 
 . 
 conversionActionId 
  
 = 
  
 Long 
 . 
 parseLong 
 ( 
 "INSERT_CONVERSION_ACTION_ID_HERE" 
 ); 
  
 params 
 . 
 orderId 
  
 = 
  
 "INSERT_ORDER_ID_HERE" 
 ; 
  
 // Optional: Specify the conversion date/time and user agent. 
  
 params 
 . 
 conversionDateTime 
  
 = 
  
 null 
 ; 
  
 params 
 . 
 userAgent 
  
 = 
  
 null 
 ; 
  
 } 
  
 GoogleAdsClient 
  
 googleAdsClient 
  
 = 
  
 null 
 ; 
  
 try 
  
 { 
  
 googleAdsClient 
  
 = 
  
 GoogleAdsClient 
 . 
 newBuilder 
 (). 
 fromPropertiesFile 
 (). 
 build 
 (); 
  
 } 
  
 catch 
  
 ( 
 FileNotFoundException 
  
 fnfe 
 ) 
  
 { 
  
 System 
 . 
 err 
 . 
 printf 
 ( 
  
 "Failed to load GoogleAdsClient configuration from file. Exception: %s%n" 
 , 
  
 fnfe 
 ); 
  
 System 
 . 
 exit 
 ( 
 1 
 ); 
  
 } 
  
 catch 
  
 ( 
 IOException 
  
 ioe 
 ) 
  
 { 
  
 System 
 . 
 err 
 . 
 printf 
 ( 
 "Failed to create GoogleAdsClient. Exception: %s%n" 
 , 
  
 ioe 
 ); 
  
 System 
 . 
 exit 
 ( 
 1 
 ); 
  
 } 
  
 try 
  
 { 
  
 new 
  
 UploadEnhancedConversionsForWeb 
 () 
  
 . 
 runExample 
 ( 
  
 googleAdsClient 
 , 
  
 params 
 . 
 customerId 
 , 
  
 params 
 . 
 conversionActionId 
 , 
  
 params 
 . 
 orderId 
 , 
  
 params 
 . 
 conversionDateTime 
 , 
  
 params 
 . 
 userAgent 
 ); 
  
 } 
  
 catch 
  
 ( 
 GoogleAdsException 
  
 gae 
 ) 
  
 { 
  
 // GoogleAdsException is the base class for most exceptions thrown by an API request. 
  
 // Instances of this exception have a message and a GoogleAdsFailure that contains a 
  
 // collection of GoogleAdsErrors that indicate the underlying causes of the 
  
 // GoogleAdsException. 
  
 System 
 . 
 err 
 . 
 printf 
 ( 
  
 "Request ID %s failed due to GoogleAdsException. Underlying errors:%n" 
 , 
  
 gae 
 . 
 getRequestId 
 ()); 
  
 int 
  
 i 
  
 = 
  
 0 
 ; 
  
 for 
  
 ( 
 GoogleAdsError 
  
 googleAdsError 
  
 : 
  
 gae 
 . 
 getGoogleAdsFailure 
 (). 
 getErrorsList 
 ()) 
  
 { 
  
 System 
 . 
 err 
 . 
 printf 
 ( 
 "  Error %d: %s%n" 
 , 
  
 i 
 ++ 
 , 
  
 googleAdsError 
 ); 
  
 } 
  
 System 
 . 
 exit 
 ( 
 1 
 ); 
  
 } 
  
 } 
  
 /** 
 * Runs the example. 
 * 
 * @param googleAdsClient the Google Ads API client. 
 * @param customerId the client customer ID. 
 * @param conversionActionId conversion action ID associated with this conversion. 
 * @param orderId unique order ID (transaction ID) of the conversion. 
 * @param conversionDateTime date and time of the conversion. 
 * @param userAgent the HTTP user agent of the conversion. 
 */ 
  
 private 
  
 void 
  
 runExample 
 ( 
  
 GoogleAdsClient 
  
 googleAdsClient 
 , 
  
 long 
  
 customerId 
 , 
  
 long 
  
 conversionActionId 
 , 
  
 String 
  
 orderId 
 , 
  
 String 
  
 conversionDateTime 
 , 
  
 String 
  
 userAgent 
 ) 
  
 throws 
  
 NoSuchAlgorithmException 
 , 
  
 UnsupportedEncodingException 
  
 { 
  
 // Creates a builder for constructing the enhancement adjustment. 
  
 ConversionAdjustment 
 . 
 Builder 
  
 enhancementBuilder 
  
 = 
  
 ConversionAdjustment 
 . 
 newBuilder 
 (). 
 setAdjustmentType 
 ( 
 ConversionAdjustmentType 
 . 
 ENHANCEMENT 
 ); 
  
 // Extracts user email, phone, and address info from the raw data, normalizes and hashes it, 
  
 // then wraps it in UserIdentifier objects. 
  
 // Creates a separate UserIdentifier object for each. The data in this example is hardcoded, but 
  
 // in your application you might read the raw data from an input file. 
  
 // IMPORTANT: Since the identifier attribute of UserIdentifier 
  
 // (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier) is a 
  
 // oneof 
  
 // (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must set only ONE of 
  
 // hashedEmail, hashedPhoneNumber, mobileId, thirdPartyUserId, or addressInfo. Setting more 
  
 // than one of these attributes on the same UserIdentifier will clear all the other members 
  
 // of the oneof. For example, the following code is INCORRECT and will result in a 
  
 // UserIdentifier with ONLY a hashedPhoneNumber. 
  
 // 
  
 // UserIdentifier incorrectlyPopulatedUserIdentifier = 
  
 //     UserIdentifier.newBuilder() 
  
 //         .setHashedEmail("...") 
  
 //         .setHashedPhoneNumber("...") 
  
 //         .build(); 
  
 ImmutableMap 
 . 
 Builder<String 
 , 
  
 String 
>  
 rawRecordBuilder 
  
 = 
  
 ImmutableMap 
 . 
< String 
 , 
  
 String>builder 
 () 
  
 . 
 put 
 ( 
 "email" 
 , 
  
 "alex.2@example.com" 
 ) 
  
 // Email address that includes a period (.) before the Gmail domain. 
  
 . 
 put 
 ( 
 "email" 
 , 
  
 "alex.2@example.com" 
 ) 
  
 // Address that includes all four required elements: first name, last name, country 
  
 // code, and postal code. 
  
 . 
 put 
 ( 
 "firstName" 
 , 
  
 "Alex" 
 ) 
  
 . 
 put 
 ( 
 "lastName" 
 , 
  
 "Quinn" 
 ) 
  
 . 
 put 
 ( 
 "countryCode" 
 , 
  
 "US" 
 ) 
  
 . 
 put 
 ( 
 "postalCode" 
 , 
  
 "94045" 
 ) 
  
 // Phone number to be converted to E.164 format, with a leading '+' as required. 
  
 . 
 put 
 ( 
 "phone" 
 , 
  
 "+1 800 5550102" 
 ) 
  
 // This example lets you put conversion details as arguments, but in reality you might 
  
 // store this data alongside other user data, so we include it in this sample user 
  
 // record. 
  
 . 
 put 
 ( 
 "orderId" 
 , 
  
 orderId 
 ) 
  
 . 
 put 
 ( 
 "conversionActionId" 
 , 
  
 Long 
 . 
 toString 
 ( 
 conversionActionId 
 )) 
  
 . 
 put 
 ( 
 "currencyCode" 
 , 
  
 "USD" 
 ); 
  
 // Adds entries for the optional fields. 
  
 if 
  
 ( 
 conversionDateTime 
  
 != 
  
 null 
 ) 
  
 { 
  
 rawRecordBuilder 
 . 
 put 
 ( 
 "conversionDateTime" 
 , 
  
 conversionDateTime 
 ); 
  
 } 
  
 if 
  
 ( 
 userAgent 
  
 != 
  
 null 
 ) 
  
 { 
  
 rawRecordBuilder 
 . 
 put 
 ( 
 "userAgent" 
 , 
  
 userAgent 
 ); 
  
 } 
  
 // Builds the map representing the record. 
  
 Map<String 
 , 
  
 String 
>  
 rawRecord 
  
 = 
  
 rawRecordBuilder 
 . 
 build 
 (); 
  
 // Creates a SHA256 message digest for hashing user identifiers in a privacy-safe way, as 
  
 // described at https://support.google.com/google-ads/answer/9888656. 
  
 MessageDigest 
  
 sha256Digest 
  
 = 
  
 MessageDigest 
 . 
 getInstance 
 ( 
 "SHA-256" 
 ); 
  
 // Creates a list for the user identifiers. 
  
 List<UserIdentifier> 
  
 userIdentifiers 
  
 = 
  
 new 
  
 ArrayList 
<> (); 
  
 // Creates a user identifier using the hashed email address, using the normalize and hash method 
  
 // specifically for email addresses. 
  
 UserIdentifier 
  
 emailIdentifier 
  
 = 
  
 UserIdentifier 
 . 
 newBuilder 
 () 
  
 // Optional: specify the user identifier source. 
  
 . 
 setUserIdentifierSource 
 ( 
 UserIdentifierSource 
 . 
 FIRST_PARTY 
 ) 
  
 // Uses the normalize and hash method specifically for email addresses. 
  
 . 
 setHashedEmail 
 ( 
 normalizeAndHashEmailAddress 
 ( 
 sha256Digest 
 , 
  
 rawRecord 
 . 
 get 
 ( 
 "email" 
 ))) 
  
 . 
 build 
 (); 
  
 userIdentifiers 
 . 
 add 
 ( 
 emailIdentifier 
 ); 
  
 // Checks if the record has a phone number, and if so, adds a UserIdentifier for it. 
  
 if 
  
 ( 
 rawRecord 
 . 
 containsKey 
 ( 
 "phone" 
 )) 
  
 { 
  
 UserIdentifier 
  
 hashedPhoneNumberIdentifier 
  
 = 
  
 UserIdentifier 
 . 
 newBuilder 
 () 
  
 . 
 setHashedPhoneNumber 
 ( 
 normalizeAndHash 
 ( 
 sha256Digest 
 , 
  
 rawRecord 
 . 
 get 
 ( 
 "phone" 
 ), 
  
 true 
 )) 
  
 . 
 build 
 (); 
  
 // Adds the hashed phone number identifier to the UserData object's list. 
  
 userIdentifiers 
 . 
 add 
 ( 
 hashedPhoneNumberIdentifier 
 ); 
  
 } 
  
 // Checks if the record has all the required mailing address elements, and if so, adds a 
  
 // UserIdentifier for the mailing address. 
  
 if 
  
 ( 
 rawRecord 
 . 
 containsKey 
 ( 
 "firstName" 
 )) 
  
 { 
  
 // Checks if the record contains all the other required elements of a mailing address. 
  
 Set<String> 
  
 missingAddressKeys 
  
 = 
  
 new 
  
 HashSet 
<> (); 
  
 for 
  
 ( 
 String 
  
 addressKey 
  
 : 
  
 new 
  
 String 
 [] 
  
 { 
 "lastName" 
 , 
  
 "countryCode" 
 , 
  
 "postalCode" 
 }) 
  
 { 
  
 if 
  
 ( 
 ! 
 rawRecord 
 . 
 containsKey 
 ( 
 addressKey 
 )) 
  
 { 
  
 missingAddressKeys 
 . 
 add 
 ( 
 addressKey 
 ); 
  
 } 
  
 } 
  
 if 
  
 ( 
 ! 
 missingAddressKeys 
 . 
 isEmpty 
 ()) 
  
 { 
  
 System 
 . 
 out 
 . 
 printf 
 ( 
  
 "Skipping addition of mailing address information because the following required keys" 
  
 + 
  
 " are missing: %s%n" 
 , 
  
 missingAddressKeys 
 ); 
  
 } 
  
 else 
  
 { 
  
 // Creates an OfflineUserAddressInfo object that contains all the required elements of a 
  
 // mailing address. 
  
 OfflineUserAddressInfo 
  
 addressInfo 
  
 = 
  
 OfflineUserAddressInfo 
 . 
 newBuilder 
 () 
  
 . 
 setHashedFirstName 
 ( 
  
 normalizeAndHash 
 ( 
 sha256Digest 
 , 
  
 rawRecord 
 . 
 get 
 ( 
 "firstName" 
 ), 
  
 false 
 )) 
  
 . 
 setHashedLastName 
 ( 
 normalizeAndHash 
 ( 
 sha256Digest 
 , 
  
 rawRecord 
 . 
 get 
 ( 
 "lastName" 
 ), 
  
 false 
 )) 
  
 . 
 setCountryCode 
 ( 
 rawRecord 
 . 
 get 
 ( 
 "countryCode" 
 )) 
  
 . 
 setPostalCode 
 ( 
 rawRecord 
 . 
 get 
 ( 
 "postalCode" 
 )) 
  
 . 
 build 
 (); 
  
 UserIdentifier 
  
 addressIdentifier 
  
 = 
  
 UserIdentifier 
 . 
 newBuilder 
 (). 
 setAddressInfo 
 ( 
 addressInfo 
 ). 
 build 
 (); 
  
 // Adds the address identifier to the UserData object's list. 
  
 userIdentifiers 
 . 
 add 
 ( 
 addressIdentifier 
 ); 
  
 } 
  
 } 
  
 // Adds the user identifiers to the enhancement adjustment. 
  
 enhancementBuilder 
 . 
 addAllUserIdentifiers 
 ( 
 userIdentifiers 
 ); 
  
 // Sets the conversion action. 
  
 enhancementBuilder 
 . 
 setConversionAction 
 ( 
  
 ResourceNames 
 . 
 conversionAction 
 ( 
  
 customerId 
 , 
  
 Long 
 . 
 parseLong 
 ( 
 rawRecord 
 . 
 get 
 ( 
 "conversionActionId" 
 )))); 
  
 // Sets the order ID. Enhancements MUST use order ID instead of GCLID date/time pair. 
  
 enhancementBuilder 
 . 
 setOrderId 
 ( 
 rawRecord 
 . 
 get 
 ( 
 "orderId" 
 )); 
  
 // Sets the conversion date and time if provided. Providing this value is optional but 
  
 // recommended. 
  
 if 
  
 ( 
 rawRecord 
 . 
 containsKey 
 ( 
 "conversionDateTime" 
 )) 
  
 { 
  
 enhancementBuilder 
 . 
 setGclidDateTimePair 
 ( 
  
 GclidDateTimePair 
 . 
 newBuilder 
 () 
  
 . 
 setConversionDateTime 
 ( 
 rawRecord 
 . 
 get 
 ( 
 "conversionDateTime" 
 ))); 
  
 } 
  
 // Sets the user agent if provided. This should match the user agent of the request that sent 
  
 // the original conversion so the conversion and its enhancement are either both attributed as 
  
 // same-device or both attributed as cross-device. 
  
 if 
  
 ( 
 rawRecord 
 . 
 containsKey 
 ( 
 "userAgent" 
 )) 
  
 { 
  
 enhancementBuilder 
 . 
 setUserAgent 
 ( 
 rawRecord 
 . 
 get 
 ( 
 "userAgent" 
 )); 
  
 } 
  
 // Creates the conversion adjustment upload service client. 
  
 try 
  
 ( 
 ConversionAdjustmentUploadServiceClient 
  
 conversionUploadServiceClient 
  
 = 
  
 googleAdsClient 
 . 
 getLatestVersion 
 (). 
 createConversionAdjustmentUploadServiceClient 
 ()) 
  
 { 
  
 // Uploads the enhancement adjustment. Partial failure should always be set to true. 
  
 // NOTE: This request contains a single adjustment as a demonstration. However, if you have 
  
 // multiple adjustments to upload, it's best to upload multiple adjustments per request 
  
 // instead of sending a separate request per adjustment. See the following for per-request 
  
 // limits: 
  
 // https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_adjustment_upload_service 
  
 UploadConversionAdjustmentsResponse 
  
 response 
  
 = 
  
 conversionUploadServiceClient 
 . 
 uploadConversionAdjustments 
 ( 
  
 UploadConversionAdjustmentsRequest 
 . 
 newBuilder 
 () 
  
 . 
 setCustomerId 
 ( 
 Long 
 . 
 toString 
 ( 
 customerId 
 )) 
  
 . 
 addConversionAdjustments 
 ( 
 enhancementBuilder 
 ) 
  
 // Enables partial failure (must be true). 
  
 . 
 setPartialFailure 
 ( 
 true 
 ) 
  
 . 
 build 
 ()); 
  
 // Prints any partial errors returned. 
  
 // To review the overall health of your recent uploads, see: 
  
 // https://developers.google.com/google-ads/api/docs/conversions/upload-summaries 
  
 if 
  
 ( 
 response 
 . 
 hasPartialFailureError 
 ()) 
  
 { 
  
 System 
 . 
 out 
 . 
 printf 
 ( 
  
 "Partial error encountered: '%s'.%n" 
 , 
  
 response 
 . 
 getPartialFailureError 
 (). 
 getMessage 
 ()); 
  
 } 
  
 else 
  
 { 
  
 // Prints the result. 
  
 ConversionAdjustmentResult 
  
 result 
  
 = 
  
 response 
 . 
 getResults 
 ( 
 0 
 ); 
  
 System 
 . 
 out 
 . 
 printf 
 ( 
  
 "Uploaded conversion adjustment of '%s' for order ID '%s'.%n" 
 , 
  
 result 
 . 
 getConversionAction 
 (), 
  
 result 
 . 
 getOrderId 
 ()); 
  
 } 
  
 } 
  
 } 
  
 /** 
 * Returns the result of normalizing and then hashing the string using the provided digest. 
 * Private customer data must be hashed during upload, as described at 
 * https://support.google.com/google-ads/answer/7474263. 
 * 
 * @param digest the digest to use to hash the normalized string. 
 * @param s the string to normalize and hash. 
 * @param trimIntermediateSpaces if true, removes leading, trailing, and intermediate spaces from 
 *     the string before hashing. If false, only removes leading and trailing spaces from the 
 *     string before hashing. 
 */ 
  
 private 
  
 String 
  
 normalizeAndHash 
 ( 
 MessageDigest 
  
 digest 
 , 
  
 String 
  
 s 
 , 
  
 boolean 
  
 trimIntermediateSpaces 
 ) 
  
 throws 
  
 UnsupportedEncodingException 
  
 { 
  
 // Normalizes by first converting all characters to lowercase, then trimming spaces. 
  
 String 
  
 normalized 
  
 = 
  
 s 
 . 
 toLowerCase 
 (); 
  
 if 
  
 ( 
 trimIntermediateSpaces 
 ) 
  
 { 
  
 // Removes leading, trailing, and intermediate spaces. 
  
 normalized 
  
 = 
  
 normalized 
 . 
 replaceAll 
 ( 
 "\\s+" 
 , 
  
 "" 
 ); 
  
 } 
  
 else 
  
 { 
  
 // Removes only leading and trailing spaces. 
  
 normalized 
  
 = 
  
 normalized 
 . 
 trim 
 (); 
  
 } 
  
 // Hashes the normalized string using the hashing algorithm. 
  
 byte 
 [] 
  
 hash 
  
 = 
  
 digest 
 . 
 digest 
 ( 
 normalized 
 . 
 getBytes 
 ( 
 "UTF-8" 
 )); 
  
 StringBuilder 
  
 result 
  
 = 
  
 new 
  
 StringBuilder 
 (); 
  
 for 
  
 ( 
 byte 
  
 b 
  
 : 
  
 hash 
 ) 
  
 { 
  
 result 
 . 
 append 
 ( 
 String 
 . 
 format 
 ( 
 "%02x" 
 , 
  
 b 
 )); 
  
 } 
  
 return 
  
 result 
 . 
 toString 
 (); 
  
 } 
  
 /** 
 * Returns the result of normalizing and hashing an email address. For this use case, Google Ads 
 * requires removal of any '.' characters preceding {@code gmail.com} or {@code googlemail.com}. 
 * 
 * @param digest the digest to use to hash the normalized string. 
 * @param emailAddress the email address to normalize and hash. 
 */ 
  
 private 
  
 String 
  
 normalizeAndHashEmailAddress 
 ( 
 MessageDigest 
  
 digest 
 , 
  
 String 
  
 emailAddress 
 ) 
  
 throws 
  
 UnsupportedEncodingException 
  
 { 
  
 String 
  
 normalizedEmail 
  
 = 
  
 emailAddress 
 . 
 toLowerCase 
 (); 
  
 String 
 [] 
  
 emailParts 
  
 = 
  
 normalizedEmail 
 . 
 split 
 ( 
 "@" 
 ); 
  
 if 
  
 ( 
 emailParts 
 . 
 length 
 > 
 1 
 && 
 emailParts 
 [ 
 1 
 ] 
 . 
 matches 
 ( 
 "^(gmail|googlemail)\\.com\\s*" 
 )) 
  
 { 
  
 // Removes any '.' characters from the portion of the email address before the domain if the 
  
 // domain is gmail.com or googlemail.com. 
  
 emailParts 
 [ 
 0 
 ] 
  
 = 
  
 emailParts 
 [ 
 0 
 ] 
 . 
 replaceAll 
 ( 
 "\\." 
 , 
  
 "" 
 ); 
  
 normalizedEmail 
  
 = 
  
 String 
 . 
 format 
 ( 
 "%s@%s" 
 , 
  
 emailParts 
 [ 
 0 
 ] 
 , 
  
 emailParts 
 [ 
 1 
 ] 
 ); 
  
 } 
  
 return 
  
 normalizeAndHash 
 ( 
 digest 
 , 
  
 normalizedEmail 
 , 
  
 true 
 ); 
  
 } 
 } 
  
  

C#

 // Copyright 2021 Google LLC 
 // 
 // Licensed under the Apache License, Version 2.0 (the "License"); 
 // you may not use this file except in compliance with the License. 
 // You may obtain a copy of the License at 
 // 
 //     http://www.apache.org/licenses/LICENSE-2.0 
 // 
 // Unless required by applicable law or agreed to in writing, software 
 // distributed under the License is distributed on an "AS IS" BASIS, 
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 // See the License for the specific language governing permissions and 
 // limitations under the License. 
 using 
  
 CommandLine 
 ; 
 using 
  
 Google.Ads.Gax.Examples 
 ; 
 using 
  
 Google.Ads.GoogleAds.Lib 
 ; 
 using 
  
 Google.Ads.GoogleAds.V21.Common 
 ; 
 using 
  
 Google.Ads.GoogleAds.V21.Errors 
 ; 
 using 
  
 Google.Ads.GoogleAds.V21.Resources 
 ; 
 using 
  
 Google.Ads.GoogleAds.V21.Services 
 ; 
 using 
  
 System 
 ; 
 using 
  
 System.Collections.Generic 
 ; 
 using 
  
 System.Security.Cryptography 
 ; 
 using 
  
 System.Text 
 ; 
 using 
  
 static 
  
 Google 
 . 
 Ads 
 . 
 GoogleAds 
 . 
 V21 
 . 
 Enums 
 . 
 ConversionAdjustmentTypeEnum 
 . 
 Types 
 ; 
 using 
  
 static 
  
 Google 
 . 
 Ads 
 . 
 GoogleAds 
 . 
 V21 
 . 
 Enums 
 . 
 UserIdentifierSourceEnum 
 . 
 Types 
 ; 
 namespace 
  
 Google.Ads.GoogleAds.Examples.V21 
 { 
  
 /// <summary> 
  
 /// This code example enhances a web conversion by uploading a {@link ConversionAdjustment} 
  
 /// containing hashed user identifiers and an order ID. 
  
 /// </summary> 
  
 public 
  
 class 
  
 UploadEnhancedConversionsForWeb 
  
 : 
  
 ExampleBase 
  
 { 
  
 /// <summary> 
  
 /// Command line options for running the <see cref="UploadEnhancedConversionsForWeb"/> example. 
  
 /// </summary> 
  
 public 
  
 class 
  
 Options 
  
 : 
  
 OptionsBase 
  
 { 
  
 /// <summary> 
  
 /// The Google Ads customer ID for which the conversion action is added. 
  
 /// </summary> 
  
 [Option("customerId", Required = true, HelpText = 
 "The Google Ads customer ID for which the conversion action is added.")] 
  
 public 
  
 long 
  
 CustomerId 
  
 { 
  
 get 
 ; 
  
 set 
 ; 
  
 } 
  
 /// <summary> 
  
 /// ID of the conversion action for which adjustments are uploaded. 
  
 /// </summary> 
  
 [Option("conversionActionId", Required = true, HelpText = 
 "ID of the conversion action for which adjustments are uploaded.")] 
  
 public 
  
 long 
  
 ConversionActionId 
  
 { 
  
 get 
 ; 
  
 set 
 ; 
  
 } 
  
 /// <summary> 
  
 /// The unique order ID (transaction ID) of the conversion. 
  
 /// </summary> 
  
 [Option("orderId", Required = true, HelpText = 
 "The unique order ID (transaction ID) of the conversion.")] 
  
 public 
  
 string 
  
 OrderId 
  
 { 
  
 get 
 ; 
  
 set 
 ; 
  
 } 
  
 /// <summary> 
  
 /// The date time at which the conversion with the specified order ID occurred. Must 
  
 /// be after the click time, and must include the time zone offset. The format is 
  
 /// 'yyyy-mm-dd hh:mm:ss+|-hh:mm', e.g. '2019-01-01 12:32:45-08:00'. Setting this 
  
 /// field is optional, but recommended. 
  
 /// </summary> 
  
 [Option("conversionDateTime", Required = false, HelpText = 
 "The date time at which the conversion with the specified order ID occurred. " + 
 "Must be after the click time, and must include the time zone offset. The " + 
 "format is 'yyyy-mm-dd hh:mm:ss+|-hh:mm', e.g. '2019-01-01 12:32:45-08:00'. " + 
 "Setting this field is optional, but recommended.")] 
  
 public 
  
 string 
  
 ConversionDateTime 
  
 { 
  
 get 
 ; 
  
 set 
 ; 
  
 } 
  
 /// <summary> 
  
 /// The HTTP user agent of the conversion. 
  
 /// </summary> 
  
 [Option("userAgent", Required = true, HelpText = 
 "The HTTP user agent of the conversion.")] 
  
 public 
  
 string 
  
 UserAgent 
  
 { 
  
 get 
 ; 
  
 set 
 ; 
  
 } 
  
 } 
  
 /// <summary> 
  
 /// Main method, to run this code example as a standalone application. 
  
 /// </summary> 
  
 /// <param name="args">The command line arguments.</param> 
  
 public 
  
 static 
  
 void 
  
 Main 
 ( 
 string 
 [] 
  
 args 
 ) 
  
 { 
  
 Options 
  
 options 
  
 = 
  
 ExampleUtilities 
 . 
 ParseCommandLine<Options> 
 ( 
 args 
 ); 
  
 UploadEnhancedConversionsForWeb 
  
 codeExample 
  
 = 
  
 new 
  
 UploadEnhancedConversionsForWeb 
 (); 
  
 Console 
 . 
 WriteLine 
 ( 
 codeExample 
 . 
 Description 
 ); 
  
 codeExample 
 . 
 Run 
 ( 
 new 
  
 GoogleAdsClient 
 (), 
  
 options 
 . 
 CustomerId 
 , 
  
 options 
 . 
 ConversionActionId 
 , 
  
 options 
 . 
 OrderId 
 , 
  
 options 
 . 
 ConversionDateTime 
 , 
  
 options 
 . 
 UserAgent 
 ); 
  
 } 
  
 private 
  
 static 
  
 SHA256 
  
 digest 
  
 = 
  
 SHA256 
 . 
 Create 
 (); 
  
 /// <summary> 
  
 /// Returns a description about the code example. 
  
 /// </summary> 
  
 public 
  
 override 
  
 string 
  
 Description 
  
 = 
>  
 "This code example adjusts an existing conversion by supplying user identifiers so " 
  
 + 
  
 "Google can enhance the conversion value." 
 ; 
  
 /// <summary> 
  
 /// Runs the code example. 
  
 /// </summary> 
  
 /// <param name="client">The Google Ads client.</param> 
  
 /// <param name="customerId">The Google Ads customer ID.</param> 
  
 /// <param name="conversionActionId">ID of the conversion action associated with this 
  
 /// conversion.</param> 
  
 /// <param name="orderId">The unique order ID (transaction ID) of the conversion.</param> 
  
 /// <param name="conversionDateTime">The date time at which the conversion with the 
  
 /// specified order ID occurred.</param> 
  
 /// <param name="userAgent">The HTTP user agent of the conversion.</param> 
  
 public 
  
 void 
  
 Run 
 ( 
 GoogleAdsClient 
  
 client 
 , 
  
 long 
  
 customerId 
 , 
  
 long 
  
 conversionActionId 
 , 
  
 string 
  
 orderId 
 , 
  
 string 
  
 conversionDateTime 
 , 
  
 string 
  
 userAgent 
 ) 
  
 { 
  
 // Get the ConversionAdjustmentUploadService. 
  
 ConversionAdjustmentUploadServiceClient 
  
 conversionAdjustmentUploadService 
  
 = 
  
 client 
 . 
 GetService 
 ( 
 Services 
 . 
 V21 
 . 
 ConversionAdjustmentUploadService 
 ); 
  
 // Creates the enhancement adjustment. 
  
 ConversionAdjustment 
  
 enhancement 
  
 = 
  
 new 
  
 ConversionAdjustment 
 () 
  
 { 
  
 AdjustmentType 
  
 = 
  
 ConversionAdjustmentType 
 . 
 Enhancement 
  
 }; 
  
 // Normalize and hash the raw data, then wrap it in UserIdentifier objects. 
  
 // Create a separate UserIdentifier object for each. The data in this example is 
  
 // hardcoded, but in your application you might read the raw data from an input file. 
  
 // 
  
 // IMPORTANT: Since the identifier attribute of UserIdentifier 
  
 // (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier) 
  
 // is a oneof 
  
 // (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must set 
  
 // only ONE of hashed_email, hashed_phone_number, mobile_id, third_party_user_id, 
  
 // or address-info. Setting more than one of these attributes on the same UserIdentifier 
  
 // will clear all the other members of the oneof. For example, the following code is 
  
 // INCORRECT and will result in a UserIdentifier with ONLY a hashed_phone_number: 
  
 // UserIdentifier incorrectlyPopulatedUserIdentifier = new UserIdentifier() 
  
 // { 
  
 //         HashedEmail = "..." 
  
 //         HashedPhoneNumber = "..." 
  
 // } 
  
 UserIdentifier 
  
 addressIdentifier 
  
 = 
  
 new 
  
 UserIdentifier 
 () 
  
 { 
  
 AddressInfo 
  
 = 
  
 new 
  
 OfflineUserAddressInfo 
 () 
  
 { 
  
 HashedFirstName 
  
 = 
  
 NormalizeAndHash 
 ( 
 "Dana" 
 ), 
  
 HashedLastName 
  
 = 
  
 NormalizeAndHash 
 ( 
 "Quinn" 
 ), 
  
 HashedStreetAddress 
  
 = 
  
 NormalizeAndHash 
 ( 
 "1600 Amphitheatre Pkwy" 
 ), 
  
 City 
  
 = 
  
 "Mountain View" 
 , 
  
 State 
  
 = 
  
 "CA" 
 , 
  
 PostalCode 
  
 = 
  
 "94043" 
 , 
  
 CountryCode 
  
 = 
  
 "US" 
  
 }, 
  
 // Optional: Specifies the user identifier source. 
  
 UserIdentifierSource 
  
 = 
  
 UserIdentifierSource 
 . 
 FirstParty 
  
 }; 
  
 // Creates a user identifier using the hashed email address. 
  
 UserIdentifier 
  
 emailIdentifier 
  
 = 
  
 new 
  
 UserIdentifier 
 () 
  
 { 
  
 UserIdentifierSource 
  
 = 
  
 UserIdentifierSource 
 . 
 FirstParty 
 , 
  
 // Uses the normalize and hash method specifically for email addresses. 
  
 HashedEmail 
  
 = 
  
 NormalizeAndHashEmailAddress 
 ( 
 "dana@example.com" 
 ) 
  
 }; 
  
 // Adds the user identifiers to the enhancement adjustment. 
  
 enhancement 
 . 
 UserIdentifiers 
 . 
 AddRange 
 ( 
 new 
 [] 
  
 { 
  
 addressIdentifier 
 , 
  
 emailIdentifier 
  
 }); 
  
 // Set the conversion action. 
  
 enhancement 
 . 
 ConversionAction 
  
 = 
  
 ResourceNames 
 . 
 ConversionAction 
 ( 
 customerId 
 , 
  
 conversionActionId 
 ); 
  
 // Set the order ID. Enhancements MUST use order ID instead of GCLID date/time pair. 
  
 enhancement 
 . 
 OrderId 
  
 = 
  
 orderId 
 ; 
  
 // Sets the conversion date and time if provided. Providing this value is optional but 
  
 // recommended. 
  
 if 
  
 ( 
 string 
 . 
 IsNullOrEmpty 
 ( 
 conversionDateTime 
 )) 
  
 { 
  
 enhancement 
 . 
 GclidDateTimePair 
  
 = 
  
 new 
  
 GclidDateTimePair 
 () 
  
 { 
  
 ConversionDateTime 
  
 = 
  
 conversionDateTime 
  
 }; 
  
 } 
  
 // Sets optional fields where a value was provided. 
  
 if 
  
 ( 
 ! 
 string 
 . 
 IsNullOrEmpty 
 ( 
 userAgent 
 )) 
  
 { 
  
 // Sets the user agent. This should match the user agent of the request that 
  
 // sent the original conversion so the conversion and its enhancement are either 
  
 // both attributed as same-device or both attributed as cross-device. 
  
 enhancement 
 . 
 UserAgent 
  
 = 
  
 userAgent 
 ; 
  
 } 
  
 try 
  
 { 
  
 // Uploads the enhancement adjustment. Partial failure should always be set to true. 
  
 // 
  
 // NOTE: This request contains a single adjustment as a demonstration. 
  
 // However, if you have multiple adjustments to upload, it's best to upload 
  
 // multiple adjustmenst per request instead of sending a separate request per 
  
 // adjustment. See the following for per-request limits: 
  
 // https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_adjust 
  
 UploadConversionAdjustmentsResponse 
  
 response 
  
 = 
  
 conversionAdjustmentUploadService 
 . 
 UploadConversionAdjustments 
 ( 
  
 new 
  
 UploadConversionAdjustmentsRequest 
 () 
  
 { 
  
 CustomerId 
  
 = 
  
 customerId 
 . 
 ToString 
 (), 
  
 ConversionAdjustments 
  
 = 
  
 { 
  
 enhancement 
  
 }, 
  
 // Enables partial failure (must be true). 
  
 PartialFailure 
  
 = 
  
 true 
 , 
  
 }); 
  
 // Prints the status message if any partial failure error is returned. 
  
 // To review the overall health of your recent uploads, see: 
  
 // https://developers.google.com/google-ads/api/docs/conversions/upload-summaries 
  
 if 
  
 ( 
 response 
 . 
 PartialFailureError 
  
 != 
  
 null 
 ) 
  
 { 
  
 // Extracts the partial failure from the response status. 
  
 GoogleAdsFailure 
  
 partialFailure 
  
 = 
  
 response 
 . 
 PartialFailure 
 ; 
  
 Console 
 . 
 WriteLine 
 ( 
 $"{partialFailure.Errors.Count} partial failure error(s) " 
  
 + 
  
 $"occurred" 
 ); 
  
 } 
  
 else 
  
 { 
  
 // Prints the result. 
  
 ConversionAdjustmentResult 
  
 result 
  
 = 
  
 response 
 . 
 Results 
 [ 
 0 
 ]; 
  
 Console 
 . 
 WriteLine 
 ( 
 $"Uploaded conversion adjustment of " 
  
 + 
  
 $"'{result.ConversionAction}' for order ID '{result.OrderId}'." 
 ); 
  
 } 
  
 } 
  
 catch 
  
 ( 
 GoogleAdsException 
  
 e 
 ) 
  
 { 
  
 Console 
 . 
 WriteLine 
 ( 
 "Failure:" 
 ); 
  
 Console 
 . 
 WriteLine 
 ( 
 $"Message: {e.Message}" 
 ); 
  
 Console 
 . 
 WriteLine 
 ( 
 $"Failure: {e.Failure}" 
 ); 
  
 Console 
 . 
 WriteLine 
 ( 
 $"Request ID: {e.RequestId}" 
 ); 
  
 throw 
 ; 
  
 } 
  
 } 
  
 /// <summary> 
  
 /// Normalizes the email address and hashes it. For this use case, Google Ads requires 
  
 /// removal of any '.' characters preceding <code>gmail.com</code> or 
  
 /// <code>googlemail.com</code>. 
  
 /// </summary> 
  
 /// <param name="emailAddress">The email address.</param> 
  
 /// <returns>The hash code.</returns> 
  
 private 
  
 string 
  
 NormalizeAndHashEmailAddress 
 ( 
 string 
  
 emailAddress 
 ) 
  
 { 
  
 string 
  
 normalizedEmail 
  
 = 
  
 emailAddress 
 . 
 ToLower 
 (); 
  
 string 
 [] 
  
 emailParts 
  
 = 
  
 normalizedEmail 
 . 
 Split 
 ( 
 '@' 
 ); 
  
 if 
  
 ( 
 emailParts 
 . 
 Length 
 > 
 1 
 && 
 ( 
 emailParts 
 [ 
 1 
 ] 
  
 == 
  
 "gmail.com" 
  
 || 
  
 emailParts 
 [ 
 1 
 ] 
  
 == 
  
 "googlemail.com" 
 )) 
  
 { 
  
 // Removes any '.' characters from the portion of the email address before 
  
 // the domain if the domain is gmail.com or googlemail.com. 
  
 emailParts 
 [ 
 0 
 ] 
  
 = 
  
 emailParts 
 [ 
 0 
 ]. 
 Replace 
 ( 
 "." 
 , 
  
 "" 
 ); 
  
 normalizedEmail 
  
 = 
  
 $"{emailParts[0]}@{emailParts[1]}" 
 ; 
  
 } 
  
 return 
  
 NormalizeAndHash 
 ( 
 normalizedEmail 
 ); 
  
 } 
  
 /// <summary> 
  
 /// Normalizes and hashes a string value. 
  
 /// </summary> 
  
 /// <param name="value">The value to normalize and hash.</param> 
  
 /// <returns>The normalized and hashed value.</returns> 
  
 private 
  
 static 
  
 string 
  
 NormalizeAndHash 
 ( 
 string 
  
 value 
 ) 
  
 { 
  
 return 
  
 ToSha256String 
 ( 
 digest 
 , 
  
 ToNormalizedValue 
 ( 
 value 
 )); 
  
 } 
  
 /// <summary> 
  
 /// Hash a string value using SHA-256 hashing algorithm. 
  
 /// </summary> 
  
 /// <param name="digest">Provides the algorithm for SHA-256.</param> 
  
 /// <param name="value">The string value (e.g. an email address) to hash.</param> 
  
 /// <returns>The hashed value.</returns> 
  
 private 
  
 static 
  
 string 
  
 ToSha256String 
 ( 
 SHA256 
  
 digest 
 , 
  
 string 
  
 value 
 ) 
  
 { 
  
 byte 
 [] 
  
 digestBytes 
  
 = 
  
 digest 
 . 
 ComputeHash 
 ( 
 Encoding 
 . 
 UTF8 
 . 
 GetBytes 
 ( 
 value 
 )); 
  
 // Convert the byte array into an unhyphenated hexadecimal string. 
  
 return 
  
 BitConverter 
 . 
 ToString 
 ( 
 digestBytes 
 ). 
 Replace 
 ( 
 "-" 
 , 
  
 string 
 . 
 Empty 
 ); 
  
 } 
  
 /// <summary> 
  
 /// Removes leading and trailing whitespace and converts all characters to 
  
 /// lower case. 
  
 /// </summary> 
  
 /// <param name="value">The value to normalize.</param> 
  
 /// <returns>The normalized value.</returns> 
  
 private 
  
 static 
  
 string 
  
 ToNormalizedValue 
 ( 
 string 
  
 value 
 ) 
  
 { 
  
 return 
  
 value 
 . 
 Trim 
 (). 
 ToLower 
 (); 
  
 } 
  
 } 
 } 
  
  

PHP

< ?php 
 /** 
 * Copyright 2021 Google LLC 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 *     https://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 
 namespace Google\Ads\GoogleAds\Examples\Remarketing; 
 require __DIR__ . '/../../vendor/autoload.php'; 
 use GetOpt\GetOpt; 
 use Google\Ads\GoogleAds\Examples\Utils\ArgumentNames; 
 use Google\Ads\GoogleAds\Examples\Utils\ArgumentParser; 
 use Google\Ads\GoogleAds\Lib\OAuth2TokenBuilder; 
 use Google\Ads\GoogleAds\Lib\V21\GoogleAdsClient; 
 use Google\Ads\GoogleAds\Lib\V21\GoogleAdsClientBuilder; 
 use Google\Ads\GoogleAds\Lib\V21\GoogleAdsException; 
 use Google\Ads\GoogleAds\Util\V21\ResourceNames; 
 use Google\Ads\GoogleAds\V21\Common\OfflineUserAddressInfo; 
 use Google\Ads\GoogleAds\V21\Common\UserIdentifier; 
 use Google\Ads\GoogleAds\V21\Enums\ConversionAdjustmentTypeEnum\ConversionAdjustmentType; 
 use Google\Ads\GoogleAds\V21\Enums\UserIdentifierSourceEnum\UserIdentifierSource; 
 use Google\Ads\GoogleAds\V21\Errors\GoogleAdsError; 
 use Google\Ads\GoogleAds\V21\Services\ConversionAdjustment; 
 use Google\Ads\GoogleAds\V21\Services\ConversionAdjustmentResult; 
 use Google\Ads\GoogleAds\V21\Services\GclidDateTimePair; 
 use Google\Ads\GoogleAds\V21\Services\UploadConversionAdjustmentsRequest; 
 use Google\ApiCore\ApiException; 
 /** 
 * Enhances a web conversion by uploading a ConversionAdjustment containing hashed user 
 * identifiers and an order ID. 
 */ 
 class UploadEnhancedConversionsForWeb 
 { 
 private const CUSTOMER_ID = 'INSERT_CUSTOMER_ID_HERE'; 
 private const CONVERSION_ACTION_ID = 'INSERT_CONVERSION_ACTION_ID_HERE'; 
 private const ORDER_ID = 'INSERT_ORDER_ID_HERE'; 
 // Optional parameters. 
 // The date time at which the conversion with the specified order ID occurred. 
 // Must be after the click time, and must include the time zone offset. 
 // The format is "yyyy-mm-dd hh:mm:ss+|-hh:mm", e.g. '2019-01-01 12:32:45-08:00'. 
 // Setting this field is optional, but recommended. 
 private const CONVERSION_DATE_TIME = null; 
 private const USER_AGENT = null; 
 public static function main() 
 { 
 // Either pass the required parameters for this example on the command line, or insert them 
 // into the constants above. 
 $options = (new ArgumentParser())->parseCommandArguments([ 
 ArgumentNames::CUSTOMER_ID => GetOpt::REQUIRED_ARGUMENT, 
 ArgumentNames::CONVERSION_ACTION_ID => GetOpt::REQUIRED_ARGUMENT, 
 ArgumentNames::ORDER_ID => GetOpt::REQUIRED_ARGUMENT, 
 ArgumentNames::CONVERSION_DATE_TIME => GetOpt::OPTIONAL_ARGUMENT, 
 ArgumentNames::USER_AGENT => GetOpt::OPTIONAL_ARGUMENT 
 ]); 
 // Generate a refreshable OAuth2 credential for authentication. 
 $oAuth2Credential = (new OAuth2TokenBuilder())->fromFile()->build(); 
 // Construct a Google Ads client configured from a properties file and the 
 // OAuth2 credentials above. 
 $googleAdsClient = (new GoogleAdsClientBuilder()) 
 ->fromFile() 
 ->withOAuth2Credential($oAuth2Credential) 
 ->build(); 
 try { 
 self::runExample( 
 $googleAdsClient, 
 $options[ArgumentNames::CUSTOMER_ID] ?: self::CUSTOMER_ID, 
 $options[ArgumentNames::CONVERSION_ACTION_ID] ?: self::CONVERSION_ACTION_ID, 
 $options[ArgumentNames::ORDER_ID] ?: self::ORDER_ID, 
 $options[ArgumentNames::CONVERSION_DATE_TIME] ?: self::CONVERSION_DATE_TIME, 
 $options[ArgumentNames::USER_AGENT] ?: self::USER_AGENT 
 ); 
 } catch (GoogleAdsException $googleAdsException) { 
 printf( 
 "Request with ID '%s' has failed.%sGoogle Ads failure details:%s", 
 $googleAdsException->getRequestId(), 
 PHP_EOL, 
 PHP_EOL 
 ); 
 foreach ($googleAdsException->getGoogleAdsFailure()->getErrors() as $error) { 
 /** @var GoogleAdsError $error */ 
 printf( 
 "\t%s: %s%s", 
 $error->getErrorCode()->getErrorCode(), 
 $error->getMessage(), 
 PHP_EOL 
 ); 
 } 
 exit(1); 
 } catch (ApiException $apiException) { 
 printf( 
 "ApiException was thrown with message '%s'.%s", 
 $apiException->getMessage(), 
 PHP_EOL 
 ); 
 exit(1); 
 } 
 } 
 /** 
 * Runs the example. 
 * 
 * @param GoogleAdsClient $googleAdsClient the Google Ads API client 
 * @param int $customerId the customer ID 
 * @param int $conversionActionId the ID of the conversion action associated with this 
 *      conversion 
 * @param string $orderId the unique order ID (transaction ID) of the conversion 
 * @param string|null $conversionDateTime the date and time of the conversion. 
 *      The format is "yyyy-mm-dd hh:mm:ss+|-hh:mm", e.g. “2019-01-01 12:32:45-08:00” 
 * @param string|null $userAgent the HTTP user agent of the conversion 
 */ 
 public static function runExample( 
 GoogleAdsClient $googleAdsClient, 
 int $customerId, 
 int $conversionActionId, 
 string $orderId, 
 ?string $conversionDateTime, 
 ?string $userAgent 
 ) { 
 // Creates the conversion enhancement. 
 $enhancement = 
 new ConversionAdjustment(['adjustment_type' => ConversionAdjustmentType::ENHANCEMENT]); 
 // Extracts user email, phone, and address info from the raw data, normalizes and hashes it, 
 // then wraps it in UserIdentifier objects. 
 // Creates a separate UserIdentifier object for each. The data in this example is hardcoded, 
 // but in your application you might read the raw data from an input file. 
 // IMPORTANT: Since the identifier attribute of UserIdentifier 
 // (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier) is a 
 // oneof 
 // (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must set only ONE 
 // of hashedEmail, hashedPhoneNumber, mobileId, thirdPartyUserId, or addressInfo. Setting 
 // more than one of these attributes on the same UserIdentifier will clear all the other 
 // members of the oneof. For example, the following code is INCORRECT and will result in a 
 // UserIdentifier with ONLY a hashedPhoneNumber. 
 // 
 // $incorrectlyPopulatedUserIdentifier = new UserIdentifier([ 
 //    'hashed_email' => '...', 
 //    'hashed_phone_number' => '...' 
 // ]); 
 $rawRecord = [ 
 // Email address that includes a period (.) before the Gmail domain. 
 'email' => 'alex.2@example.com', 
 // Address that includes all four required elements: first name, last name, country 
 // code, and postal code. 
 'firstName' => 'Alex', 
 'lastName' => 'Quinn', 
 'countryCode' => 'US', 
 'postalCode' => '94045', 
 // Phone number to be converted to E.164 format, with a leading '+' as required. 
 'phone' => '+1 800 5550102', 
 // This example lets you input conversion details as arguments, but in reality you might 
 // store this data alongside other user data, so we include it in this sample user 
 // record. 
 'orderId' => $orderId, 
 'conversionActionId' => $conversionActionId, 
 'conversionDateTime' => $conversionDateTime, 
 'currencyCode' => 'USD' 
 ]; 
 // Creates a list for the user identifiers. 
 $userIdentifiers = []; 
 // Uses the SHA-256 hash algorithm for hashing user identifiers in a privacy-safe way, as 
 // described at https://support.google.com/google-ads/answer/9888656. 
 $hashAlgorithm = "sha256"; 
 // Creates a user identifier using the hashed email address, using the normalize and hash 
 // method specifically for email addresses. 
 $emailIdentifier = new UserIdentifier([ 
 // Uses the normalize and hash method specifically for email addresses. 
 'hashed_email' => self::normalizeAndHashEmailAddress( 
 $hashAlgorithm, 
 $rawRecord['email'] 
 ), 
 // Optional: Specifies the user identifier source. 
 'user_identifier_source' => UserIdentifierSource::FIRST_PARTY 
 ]); 
 $userIdentifiers[] = $emailIdentifier; 
 // Checks if the record has a phone number, and if so, adds a UserIdentifier for it. 
 if (array_key_exists('phone', $rawRecord)) { 
 $hashedPhoneNumberIdentifier = new UserIdentifier([ 
 'hashed_phone_number' => self::normalizeAndHash( 
 $hashAlgorithm, 
 $rawRecord['phone'], 
 true 
 ) 
 ]); 
 // Adds the hashed email identifier to the user identifiers list. 
 $userIdentifiers[] = $hashedPhoneNumberIdentifier; 
 } 
 // Checks if the record has all the required mailing address elements, and if so, adds a 
 // UserIdentifier for the mailing address. 
 if (array_key_exists('firstName', $rawRecord)) { 
 // Checks if the record contains all the other required elements of a mailing 
 // address. 
 $missingAddressKeys = []; 
 foreach (['lastName', 'countryCode', 'postalCode'] as $addressKey) { 
 if (!array_key_exists($addressKey, $rawRecord)) { 
 $missingAddressKeys[] = $addressKey; 
 } 
 } 
 if (!empty($missingAddressKeys)) { 
 printf( 
 "Skipping addition of mailing address information because the " 
 . "following required keys are missing: %s%s", 
 json_encode($missingAddressKeys), 
 PHP_EOL 
 ); 
 } else { 
 // Creates an OfflineUserAddressInfo object that contains all the required 
 // elements of a mailing address. 
 $addressIdentifier = new UserIdentifier([ 
 'address_info' => new OfflineUserAddressInfo([ 
 'hashed_first_name' => self::normalizeAndHash( 
 $hashAlgorithm, 
 $rawRecord['firstName'], 
 false 
 ), 
 'hashed_last_name' => self::normalizeAndHash( 
 $hashAlgorithm, 
 $rawRecord['lastName'], 
 false 
 ), 
 'country_code' => $rawRecord['countryCode'], 
 'postal_code' => $rawRecord['postalCode'] 
 ]) 
 ]); 
 // Adds the address identifier to the user identifiers list. 
 $userIdentifiers[] = $addressIdentifier; 
 } 
 } 
 // Adds the user identifiers to the conversion. 
 $enhancement->setUserIdentifiers($userIdentifiers); 
 // Sets the conversion action. 
 $enhancement->setConversionAction( 
 ResourceNames::forConversionAction($customerId, $rawRecord['conversionActionId']) 
 ); 
 // Sets the order ID. Enhancements MUST use order ID instead of GCLID date/time pair. 
 if (!empty($rawRecord['orderId'])) { 
 $enhancement->setOrderId($rawRecord['orderId']); 
 } 
 // Sets the conversion date and time if provided. Providing this value is optional but 
 // recommended. 
 if (!empty($rawRecord['conversionDateTime'])) { 
 // Sets the conversion date and time if provided. Providing this value is optional but 
 // recommended. 
 $enhancement->setGclidDateTimePair(new GclidDateTimePair([ 
 'conversion_date_time' => $rawRecord['conversionDateTime'] 
 ])); 
 } 
 // Sets the user agent if provided. This should match the user agent of the request that 
 // sent the original conversion so the conversion and its enhancement are either both 
 // attributed as same-device or both attributed as cross-device. 
 if (!empty($rawRecord['userAgent'])) { 
 $enhancement->setUserAgent($rawRecord['userAgent']); 
 } 
 // Issues a request to upload the conversion enhancement. 
 $conversionAdjustmentUploadServiceClient = 
 $googleAdsClient->getConversionAdjustmentUploadServiceClient(); 
 // NOTE: This request contains a single adjustment as a demonstration. However, if you have 
 // multiple adjustments to upload, it's best to upload multiple adjustments per request 
 // instead of sending a separate request per adjustment. See the following for per-request 
 // limits: 
 // https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_adjustment_upload_service 
 $response = $conversionAdjustmentUploadServiceClient->uploadConversionAdjustments( 
 // Enables partial failure (must be true). 
 UploadConversionAdjustmentsRequest::build($customerId, [$enhancement], true) 
 ); 
 // Prints the status message if any partial failure error is returned. 
 // Note: The details of each partial failure error are not printed here, you can refer to 
 // the example HandlePartialFailure.php to learn more. 
 // To review the overall health of your recent uploads, see: 
 // https://developers.google.com/google-ads/api/docs/conversions/upload-summaries 
 if ($response->hasPartialFailureError()) { 
 printf( 
 "Partial failures occurred: '%s'.%s", 
 $response->getPartialFailureError()->getMessage(), 
 PHP_EOL 
 ); 
 } else { 
 // Prints the result if exists. 
 /** @var ConversionAdjustmentResult $uploadedConversionAdjustment */ 
 $uploadedConversionAdjustment = $response->getResults()[0]; 
 printf( 
 "Uploaded conversion adjustment of '%s' for order ID '%s'.%s", 
 $uploadedConversionAdjustment->getConversionAction(), 
 $uploadedConversionAdjustment->getOrderId(), 
 PHP_EOL 
 ); 
 } 
 } 
 /** 
 * Returns the result of normalizing and then hashing the string using the provided hash 
 * algorithm. Private customer data must be hashed during upload, as described at 
 * https://support.google.com/google-ads/answer/7474263. 
 * 
 * @param string $hashAlgorithm the hash algorithm to use 
 * @param string $value the value to normalize and hash 
 * @param bool $trimIntermediateSpaces if true, removes leading, trailing, and intermediate 
 *     spaces from the string before hashing. If false, only removes leading and trailing 
 *     spaces from the string before hashing. 
 * @return string the normalized and hashed value 
 */ 
 private static function normalizeAndHash( 
 string $hashAlgorithm, 
 string $value, 
 bool $trimIntermediateSpaces 
 ): string { 
 // Normalizes by first converting all characters to lowercase, then trimming spaces. 
 $normalized = strtolower($value); 
 if ($trimIntermediateSpaces === true) { 
 // Removes leading, trailing, and intermediate spaces. 
 $normalized = str_replace(' ', '', $normalized); 
 } else { 
 // Removes only leading and trailing spaces. 
 $normalized = trim($normalized); 
 } 
 return hash($hashAlgorithm, strtolower(trim($normalized))); 
 } 
 /** 
 * Returns the result of normalizing and hashing an email address. For this use case, Google 
 * Ads requires removal of any '.' characters preceding "gmail.com" or "googlemail.com". 
 * 
 * @param string $hashAlgorithm the hash algorithm to use 
 * @param string $emailAddress the email address to normalize and hash 
 * @return string the normalized and hashed email address 
 */ 
 private static function normalizeAndHashEmailAddress( 
 string $hashAlgorithm, 
 string $emailAddress 
 ): string { 
 $normalizedEmail = strtolower($emailAddress); 
 $emailParts = explode("@", $normalizedEmail); 
 if ( 
 count($emailParts) > 1 
 && preg_match('/^(gmail|googlemail)\.com\s*/', $emailParts[1]) 
 ) { 
 // Removes any '.' characters from the portion of the email address before the domain 
 // if the domain is gmail.com or googlemail.com. 
 $emailParts[0] = str_replace(".", "", $emailParts[0]); 
 $normalizedEmail = sprintf('%s@%s', $emailParts[0], $emailParts[1]); 
 } 
 return self::normalizeAndHash($hashAlgorithm, $normalizedEmail, true); 
 } 
 } 
 UploadEnhancedConversionsForWeb::main(); 
  
  

Python

 #!/usr/bin/env python 
 # Copyright 2021 Google LLC 
 # 
 # Licensed under the Apache License, Version 2.0 (the "License"); 
 # you may not use this file except in compliance with the License. 
 # You may obtain a copy of the License at 
 # 
 #     https://www.apache.org/licenses/LICENSE-2.0 
 # 
 # Unless required by applicable law or agreed to in writing, software 
 # distributed under the License is distributed on an "AS IS" BASIS, 
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 # See the License for the specific language governing permissions and 
 # limitations under the License. 
 """Enhances a web conversion by uploading a ConversionAdjustment. 
 The conversion adjustment contains hashed user identifiers and an order ID. 
 """ 
 import 
  
 argparse 
 import 
  
 hashlib 
 import 
  
 re 
 import 
  
 sys 
 from 
  
 google.ads.googleads.client 
  
 import 
 GoogleAdsClient 
 from 
  
 google.ads.googleads.errors 
  
 import 
 GoogleAdsException 
 def 
  
 main 
 ( 
 client 
 , 
 customer_id 
 , 
 conversion_action_id 
 , 
 order_id 
 , 
 conversion_date_time 
 , 
 user_agent 
 , 
 ): 
  
 """The main method that creates all necessary entities for the example. 
 Args: 
 client: An initialized GoogleAdsClient instance. 
 customer_id: The client customer ID string. 
 conversion_action_id: The ID of the conversion action to upload to. 
 order_id: The unique ID (transaction ID) of the conversion. 
 conversion_date_time: The date and time of the conversion. 
 user_agent: The HTTP user agent of the conversion. 
 """ 
 # Extracts user email, phone, and address info from the raw data, normalizes 
 # and hashes it, then wraps it in UserIdentifier objects. Creates a separate 
 # UserIdentifier object for each. The data in this example is hardcoded, but 
 # in your application you might read the raw data from an input file. 
 # IMPORTANT: Since the identifier attribute of UserIdentifier 
 # (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier) 
 # is a oneof 
 # (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must 
 # set only ONE of hashed_email, hashed_phone_number, mobile_id, 
 # third_party_user_id, or address_info. Setting more than one of these 
 # attributes on the same UserIdentifier will clear all the other members of 
 # the oneof. For example, the following code is INCORRECT and will result in 
 # a UserIdentifier with ONLY a hashed_phone_number: 
 # 
 # incorrectly_populated_user_identifier = client.get_type("UserIdentifier") 
 # incorrectly_populated_user_identifier.hashed_email = "..."" 
 # incorrectly_populated_user_identifier.hashed_phone_number = "..."" 
 raw_record 
 = 
 { 
 # Email address that includes a period (.) before the Gmail domain. 
 "email" 
 : 
 "alex.2@example.com" 
 , 
 # Address that includes all four required elements: first name, last 
 # name, country code, and postal code. 
 "first_name" 
 : 
 "Alex" 
 , 
 "last_name" 
 : 
 "Quinn" 
 , 
 "country_code" 
 : 
 "US" 
 , 
 "postal_code" 
 : 
 "94045" 
 , 
 # Phone number to be converted to E.164 format, with a leading '+' as 
 # required. 
 "phone" 
 : 
 "+1 800 5550102" 
 , 
 # This example lets you input conversion details as arguments, but in 
 # reality you might store this data alongside other user data, so we 
 # include it in this sample user record. 
 "order_id" 
 : 
 order_id 
 , 
 "conversion_action_id" 
 : 
 conversion_action_id 
 , 
 "conversion_date_time" 
 : 
 conversion_date_time 
 , 
 "currency_code" 
 : 
 "USD" 
 , 
 "user_agent" 
 : 
 user_agent 
 , 
 } 
 # Constructs the enhancement adjustment. 
 conversion_adjustment 
 = 
 client 
 . 
 get_type 
 ( 
 "ConversionAdjustment" 
 ) 
 conversion_adjustment 
 . 
 adjustment_type 
 = 
 ( 
 client 
 . 
 enums 
 . 
 ConversionAdjustmentTypeEnum 
 . 
 ENHANCEMENT 
 ) 
 # Creates a user identifier using the hashed email address, using the 
 # normalize and hash method specifically for email addresses. 
 email_identifier 
 = 
 client 
 . 
 get_type 
 ( 
 "UserIdentifier" 
 ) 
 # Optional: Specifies the user identifier source. 
 email_identifier 
 . 
 user_identifier_source 
 = 
 ( 
 client 
 . 
 enums 
 . 
 UserIdentifierSourceEnum 
 . 
 FIRST_PARTY 
 ) 
 # Uses the normalize and hash method specifically for email addresses. 
 email_identifier 
 . 
 hashed_email 
 = 
 normalize_and_hash_email_address 
 ( 
 raw_record 
 [ 
 "email" 
 ] 
 ) 
 # Adds the email identifier to the conversion adjustment. 
 conversion_adjustment 
 . 
 user_identifiers 
 . 
 append 
 ( 
 email_identifier 
 ) 
 # Checks if the record has a phone number, and if so, adds a UserIdentifier 
 # for it. 
 if 
 raw_record 
 . 
 get 
 ( 
 "phone" 
 ) 
 is 
 not 
 None 
 : 
 phone_identifier 
 = 
 client 
 . 
 get_type 
 ( 
 "UserIdentifier" 
 ) 
 phone_identifier 
 . 
 hashed_phone_number 
 = 
 normalize_and_hash 
 ( 
 raw_record 
 [ 
 "phone" 
 ] 
 ) 
 # Adds the phone identifier to the conversion adjustment. 
 conversion_adjustment 
 . 
 user_identifiers 
 . 
 append 
 ( 
 phone_identifier 
 ) 
 # Checks if the record has all the required mailing address elements, and if 
 # so, adds a UserIdentifier for the mailing address. 
 if 
 raw_record 
 . 
 get 
 ( 
 "first_name" 
 ) 
 is 
 not 
 None 
 : 
 # Checks if the record contains all the other required elements of a 
 # mailing address. 
 required_keys 
 = 
 [ 
 "last_name" 
 , 
 "country_code" 
 , 
 "postal_code" 
 ] 
 # Builds a new list of the required keys that are missing from 
 # raw_record. 
 missing_keys 
 = 
 [ 
 key 
 for 
 key 
 in 
 required_keys 
 if 
 key 
 not 
 in 
 raw_record 
 . 
 keys 
 () 
 ] 
 if 
 len 
 ( 
 missing_keys 
 ) 
> 0 
 : 
 print 
 ( 
 "Skipping addition of mailing address information because the" 
 f 
 "following required keys are missing: 
 { 
 missing_keys 
 } 
 " 
 ) 
 else 
 : 
 # Creates a user identifier using sample values for the user address, 
 # hashing where required. 
 address_identifier 
 = 
 client 
 . 
 get_type 
 ( 
 "UserIdentifier" 
 ) 
 address_info 
 = 
 address_identifier 
 . 
 address_info 
 address_info 
 . 
 hashed_first_name 
 = 
 normalize_and_hash 
 ( 
 raw_record 
 [ 
 "first_name" 
 ] 
 ) 
 address_info 
 . 
 hashed_last_name 
 = 
 normalize_and_hash 
 ( 
 raw_record 
 [ 
 "last_name" 
 ] 
 ) 
 address_info 
 . 
 country_code 
 = 
 raw_record 
 [ 
 "country_code" 
 ] 
 address_info 
 . 
 postal_code 
 = 
 raw_record 
 [ 
 "postal_code" 
 ] 
 # Adds the address identifier to the conversion adjustment. 
 conversion_adjustment 
 . 
 user_identifiers 
 . 
 append 
 ( 
 address_identifier 
 ) 
 conversion_action_service 
 = 
 client 
 . 
 get_service 
 ( 
 "ConversionActionService" 
 ) 
 # Sets the conversion action. 
 conversion_adjustment 
 . 
 conversion_action 
 = 
 ( 
 conversion_action_service 
 . 
 conversion_action_path 
 ( 
 customer_id 
 , 
 raw_record 
 [ 
 "conversion_action_id" 
 ] 
 ) 
 ) 
 # Sets the order ID. Enhancements MUST use order ID instead of GCLID 
 # date/time pair. 
 conversion_adjustment 
 . 
 order_id 
 = 
 order_id 
 # Sets the conversion date and time if provided. Providing this value is 
 # optional but recommended. 
 if 
 raw_record 
 . 
 get 
 ( 
 "conversion_date_time" 
 ): 
 conversion_adjustment 
 . 
 gclid_date_time_pair 
 . 
 conversion_date_time 
 = 
 ( 
 raw_record 
 [ 
 "conversion_date_time" 
 ] 
 ) 
 # Sets optional fields where a value was provided 
 if 
 raw_record 
 . 
 get 
 ( 
 "user_agent" 
 ): 
 # Sets the user agent. This should match the user agent of the request 
 # that sent the original conversion so the conversion and its 
 # enhancement are either both attributed as same-device or both 
 # attributed as cross-device. 
 conversion_adjustment 
 . 
 user_agent 
 = 
 user_agent 
 # Creates the conversion adjustment upload service client. 
 conversion_adjustment_upload_service 
 = 
 client 
 . 
 get_service 
 ( 
 "ConversionAdjustmentUploadService" 
 ) 
 # Uploads the enhancement adjustment. Partial failure should always be set 
 # to true. 
 # NOTE: This request only uploads a single conversion, but if you have 
 # multiple conversions to upload, it's still best to upload them in a single 
 # request. See the following for per-request limits for reference: 
 # https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_upload_service 
 response 
 = 
 conversion_adjustment_upload_service 
 . 
 upload_conversion_adjustments 
 ( 
 customer_id 
 = 
 customer_id 
 , 
 conversion_adjustments 
 = 
 [ 
 conversion_adjustment 
 ], 
 # Enables partial failure (must be true). 
 partial_failure 
 = 
 True 
 , 
 ) 
 # Prints any partial errors returned. 
 # To review the overall health of your recent uploads, see: 
 # https://developers.google.com/google-ads/api/docs/conversions/upload-summaries 
 if 
 response 
 . 
 partial_failure_error 
 : 
 print 
 ( 
 "Partial error encountered: " 
 f 
 " 
 { 
 response 
 . 
 partial_failure_error 
 . 
 message 
 } 
 " 
 ) 
 else 
 : 
 # Prints the result. 
 result 
 = 
 response 
 . 
 results 
 [ 
 0 
 ] 
 print 
 ( 
 f 
 "Uploaded conversion adjustment of 
 { 
 result 
 . 
 conversion_action 
 } 
 for " 
 f 
 "order ID 
 { 
 result 
 , 
 order_id 
 } 
 ." 
 ) 
 def 
  
 normalize_and_hash_email_address 
 ( 
 email_address 
 ): 
  
 """Returns the result of normalizing and hashing an email address. 
 For this use case, Google Ads requires removal of any '.' characters 
 preceding "gmail.com" or "googlemail.com" 
 Args: 
 email_address: An email address to normalize. 
 Returns: 
 A normalized (lowercase, removed whitespace) and SHA-265 hashed string. 
 """ 
 normalized_email 
 = 
 email_address 
 . 
 strip 
 () 
 . 
 lower 
 () 
 email_parts 
 = 
 normalized_email 
 . 
 split 
 ( 
 "@" 
 ) 
 # Check that there are at least two segments 
 if 
 len 
 ( 
 email_parts 
 ) 
> 1 
 : 
 # Checks whether the domain of the email address is either "gmail.com" 
 # or "googlemail.com". If this regex does not match then this statement 
 # will evaluate to None. 
 if 
 re 
 . 
 match 
 ( 
 r 
 "^(gmail|googlemail)\.com$" 
 , 
 email_parts 
 [ 
 1 
 ]): 
 # Removes any '.' characters from the portion of the email address 
 # before the domain if the domain is gmail.com or googlemail.com. 
 email_parts 
 [ 
 0 
 ] 
 = 
 email_parts 
 [ 
 0 
 ] 
 . 
 replace 
 ( 
 "." 
 , 
 "" 
 ) 
 normalized_email 
 = 
 "@" 
 . 
 join 
 ( 
 email_parts 
 ) 
 return 
 normalize_and_hash 
 ( 
 normalized_email 
 ) 
 def 
  
 normalize_and_hash 
 ( 
 s 
 ): 
  
 """Normalizes and hashes a string with SHA-256. 
 Private customer data must be hashed during upload, as described at: 
 https://support.google.com/google-ads/answer/9888656 
 Args: 
 s: The string to perform this operation on. 
 Returns: 
 A normalized (lowercase, removed whitespace) and SHA-256 hashed string. 
 """ 
 return 
 hashlib 
 . 
 sha256 
 ( 
 s 
 . 
 strip 
 () 
 . 
 lower 
 () 
 . 
 encode 
 ()) 
 . 
 hexdigest 
 () 
 if 
 __name__ 
 == 
 "__main__" 
 : 
 parser 
 = 
 argparse 
 . 
 ArgumentParser 
 ( 
 description 
 = 
 "Imports offline call conversion values for calls related " 
 "to your ads." 
 ) 
 # The following argument(s) should be provided to run the example. 
 parser 
 . 
 add_argument 
 ( 
 "-c" 
 , 
 "--customer_id" 
 , 
 type 
 = 
 str 
 , 
 required 
 = 
 True 
 , 
 help 
 = 
 "The Google Ads customer ID." 
 , 
 ) 
 parser 
 . 
 add_argument 
 ( 
 "-a" 
 , 
 "--conversion_action_id" 
 , 
 type 
 = 
 str 
 , 
 required 
 = 
 True 
 , 
 help 
 = 
 "The ID of the conversion action to upload to." 
 , 
 ) 
 parser 
 . 
 add_argument 
 ( 
 "-o" 
 , 
 "--order_id" 
 , 
 type 
 = 
 str 
 , 
 required 
 = 
 True 
 , 
 help 
 = 
 "the unique ID (transaction ID) of the conversion." 
 , 
 ) 
 parser 
 . 
 add_argument 
 ( 
 "-d" 
 , 
 "--conversion_date_time" 
 , 
 type 
 = 
 str 
 , 
 help 
 = 
 "The date time at which the conversion with the specified order " 
 "ID occurred. Must be after the click time, and must include the time " 
 "zone offset.  The format is 'yyyy-mm-dd hh:mm:ss+|-hh:mm', " 
 "e.g. '2019-01-01 12:32:45-08:00'. Setting this field is optional, " 
 "but recommended" 
 , 
 ) 
 parser 
 . 
 add_argument 
 ( 
 "-u" 
 , 
 "--user_agent" 
 , 
 type 
 = 
 str 
 , 
 help 
 = 
 "The HTTP user agent of the conversion." 
 , 
 ) 
 args 
 = 
 parser 
 . 
 parse_args 
 () 
 # GoogleAdsClient will read the google-ads.yaml configuration file in the 
 # home directory if none is specified. 
 googleads_client 
 = 
 GoogleAdsClient 
 . 
 load_from_storage 
 ( 
 version 
 = 
 "v21" 
 ) 
 try 
 : 
 main 
 ( 
 googleads_client 
 , 
 args 
 . 
 customer_id 
 , 
 args 
 . 
 conversion_action_id 
 , 
 args 
 . 
 order_id 
 , 
 args 
 . 
 conversion_date_time 
 , 
 args 
 . 
 user_agent 
 , 
 ) 
 except 
 GoogleAdsException 
 as 
 ex 
 : 
 print 
 ( 
 f 
 "Request with ID ' 
 { 
 ex 
 . 
 request_id 
 } 
 '' failed with status " 
 f 
 "' 
 { 
 ex 
 . 
 error 
 . 
 code 
 () 
 . 
 name 
 } 
 ' and includes the following errors:" 
 ) 
 for 
 error 
 in 
 ex 
 . 
 failure 
 . 
 errors 
 : 
 print 
 ( 
 f 
 " 
 \t 
 Error with message ' 
 { 
 error 
 . 
 message 
 } 
 '." 
 ) 
 if 
 error 
 . 
 location 
 : 
 for 
 field_path_element 
 in 
 error 
 . 
 location 
 . 
 field_path_elements 
 : 
 print 
 ( 
 f 
 " 
 \t\t 
 On field: 
 { 
 field_path_element 
 . 
 field_name 
 } 
 " 
 ) 
 sys 
 . 
 exit 
 ( 
 1 
 ) 
  

Ruby

 #!/usr/bin/env ruby 
 # 
 # Copyright 2021 Google LLC 
 # 
 # Licensed under the Apache License, Version 2.0 (the "License"); 
 # you may not use this file except in compliance with the License. 
 # You may obtain a copy of the License at 
 # 
 #     https://www.apache.org/licenses/LICENSE-2.0 
 # 
 # Unless required by applicable law or agreed to in writing, software 
 # distributed under the License is distributed on an "AS IS" BASIS, 
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 # See the License for the specific language governing permissions and 
 # limitations under the License. 
 # 
 # Enhances a web conversion by uploading a ConversionAdjustment. 
 # The conversion adjustment contains hashed user identifiers and an order ID. 
 require 
  
 'optparse' 
 require 
  
 'google/ads/google_ads' 
 require 
  
 'digest' 
 def 
  
 upload_conversion_enhancement 
 ( 
  
 customer_id 
 , 
  
 conversion_action_id 
 , 
  
 order_id 
 , 
  
 conversion_date_time 
 , 
  
 user_agent 
 ) 
  
 # GoogleAdsClient will read a config file from 
  
 # ENV['HOME']/google_ads_config.rb when called without parameters 
  
 client 
  
 = 
  
 Google 
 :: 
 Ads 
 :: 
 GoogleAds 
 :: 
 GoogleAdsClient 
 . 
 new 
  
 # Extracts user email, phone, and address info from the raw data, normalizes 
  
 # and hashes it, then wraps it in UserIdentifier objects. Creates a separate 
  
 # UserIdentifier object for each. The data in this example is hardcoded, but 
  
 # in your application you might read the raw data from an input file. 
  
 # IMPORTANT: Since the identifier attribute of UserIdentifier 
  
 # (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier) 
  
 # is a oneof 
  
 # (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must 
  
 # set only ONE of hashed_email, hashed_phone_number, mobile_id, 
  
 # third_party_user_id, or address_info. Setting more than one of these 
  
 # attributes on the same UserIdentifier will clear all the other members of 
  
 # the oneof. For example, the following code is INCORRECT and will result in 
  
 # a UserIdentifier with ONLY a hashed_phone_number: 
  
 # 
  
 # incorrectly_populated_user_identifier.hashed_email = "..."" 
  
 # incorrectly_populated_user_identifier.hashed_phone_number = "..."" 
  
 raw_record 
  
 = 
  
 { 
  
 # Email address that includes a period (.) before the Gmail domain. 
  
 "email" 
  
 = 
>  
 "alex.2@example.com" 
 , 
  
 # Address that includes all four required elements: first name, last 
  
 # name, country code, and postal code. 
  
 "first_name" 
  
 = 
>  
 "Alex" 
 , 
  
 "last_name" 
  
 = 
>  
 "Quinn" 
 , 
  
 "country_code" 
  
 = 
>  
 "US" 
 , 
  
 "postal_code" 
  
 = 
>  
 "94045" 
 , 
  
 # Phone number to be converted to E.164 format, with a leading '+' as 
  
 # required. 
  
 "phone" 
  
 = 
>  
 "+1 800 5550102" 
 , 
  
 # This example lets you input conversion details as arguments, but in 
  
 # reality you might store this data alongside other user data, so we 
  
 # include it in this sample user record. 
  
 "order_id" 
  
 = 
>  
 order_id 
 , 
  
 "conversion_action_id" 
  
 = 
>  
 conversion_action_id 
 , 
  
 "conversion_date_time" 
  
 = 
>  
 conversion_date_time 
 , 
  
 "currency_code" 
  
 = 
>  
 "USD" 
 , 
  
 "user_agent" 
  
 = 
>  
 user_agent 
 , 
  
 } 
  
 enhancement 
  
 = 
  
 client 
 . 
 resource 
 . 
 conversion_adjustment 
  
 do 
  
 | 
 ca 
 | 
  
 ca 
 . 
 conversion_action 
  
 = 
  
 client 
 . 
 path 
 . 
 conversion_action 
 ( 
 customer_id 
 , 
  
 conversion_action_id 
 ) 
  
 ca 
 . 
 adjustment_type 
  
 = 
  
 :ENHANCEMENT 
  
 ca 
 . 
 order_id 
  
 = 
  
 order_id 
  
 # Sets the conversion date and time if provided. Providing this value is 
  
 # optional but recommended. 
  
 unless 
  
 conversion_date_time 
 . 
 nil? 
  
 ca 
 . 
 gclid_date_time_pair 
  
 = 
  
 client 
 . 
 resource 
 . 
 gclid_date_time_pair 
  
 do 
  
 | 
 pair 
 | 
  
 pair 
 . 
 conversion_date_time 
  
 = 
  
 conversion_date_time 
  
 end 
  
 end 
  
 # Creates a user identifier using the hashed email address, using the 
  
 # normalize and hash method specifically for email addresses. 
  
 ca 
 . 
 user_identifiers 
 << 
 client 
 . 
 resource 
 . 
 user_identifier 
  
 do 
  
 | 
 ui 
 | 
  
 # Uses the normalize and hash method specifically for email addresses. 
  
 ui 
 . 
 hashed_email 
  
 = 
  
 normalize_and_hash_email 
 ( 
 raw_record 
 [ 
 "email" 
 ] 
 ) 
  
 # Optional: Specifies the user identifier source. 
  
 ui 
 . 
 user_identifier_source 
  
 = 
  
 :FIRST_PARTY 
  
 end 
  
 # Checks if the record has a phone number, and if so, adds a UserIdentifier 
  
 # for it. 
  
 unless 
  
 raw_record 
 [ 
 "phone" 
 ]. 
 nil? 
  
 ca 
 . 
 user_identifiers 
 << 
 client 
 . 
 resource 
 . 
 user_identifier 
  
 do 
  
 | 
 ui 
 | 
  
 ui 
 . 
 hashed_phone_number 
  
 = 
  
 normalize_and_hash_email 
 ( 
 raw_record 
 [ 
 "phone" 
 ] 
 ) 
  
 end 
  
 end 
  
 # Checks if the record has all the required mailing address elements, and if 
  
 # so, adds a UserIdentifier for the mailing address. 
  
 unless 
  
 raw_record 
 [ 
 "first_name" 
 ]. 
 nil? 
  
 # Checks if the record contains all the other required elements of a 
  
 # mailing address. 
  
 required_keys 
  
 = 
  
 [ 
 "last_name" 
 , 
  
 "country_code" 
 , 
  
 "postal_code" 
 ] 
  
 # Builds a new list of the required keys that are missing from 
  
 # raw_record. 
  
 missing_keys 
  
 = 
  
 required_keys 
  
 - 
  
 raw_record 
 . 
 keys 
  
 if 
  
 missing_keys 
  
 puts 
 ( 
  
 "Skipping addition of mailing address information because the" 
  
 \ 
  
 "following required keys are missing: 
 #{ 
 missing_keys 
 } 
 " 
  
 ) 
  
 else 
  
 ca 
 . 
 user_identifiers 
 << 
 client 
 . 
 resource 
 . 
 user_identifier 
  
 do 
  
 | 
 ui 
 | 
  
 ui 
 . 
 address_info 
  
 = 
  
 client 
 . 
 resource 
 . 
 offline_user_address_info 
  
 do 
  
 | 
 info 
 | 
  
 # Certain fields must be hashed using SHA256 in order to handle 
  
 # identifiers in a privacy-safe way, as described at 
  
 # https://support.google.com/google-ads/answer/9888656. 
  
 info 
 . 
 hashed_first_name 
  
 = 
  
 normalize_and_hash 
 ( 
  
 raw_record 
 [ 
 "first_name" 
 ] 
 ) 
  
 info 
 . 
 hashed_last_name 
  
 = 
  
 normalize_and_hash 
 ( 
  
 raw_record 
 [ 
 "last_name" 
 ] 
 ) 
  
 info 
 . 
 postal_code 
  
 = 
  
 normalize_and_hash 
 ( 
 raw_record 
 [ 
 "country_code" 
 ] 
 ) 
  
 info 
 . 
 country_code 
  
 = 
  
 normalize_and_hash 
 ( 
 raw_record 
 [ 
 "postal_code" 
 ] 
 ) 
  
 end 
  
 end 
  
 end 
  
 end 
  
 # Sets optional fields where a value was provided. 
  
 unless 
  
 user_agent 
 . 
 nil? 
  
 # Sets the user agent. This should match the user agent of the request 
  
 # that sent the original conversion so the conversion and its enhancement 
  
 # are either both attributed as same-device or both attributed as 
  
 # cross-device. 
  
 ca 
 . 
 user_agent 
  
 = 
  
 user_agent 
  
 end 
  
 end 
  
 response 
  
 = 
  
 client 
 . 
 service 
 . 
 conversion_adjustment_upload 
 . 
 upload_conversion_adjustments 
 ( 
  
 customer_id 
 : 
  
 customer_id 
 , 
  
 # NOTE: This request only uploads a single conversion, but if you have 
  
 # multiple conversions to upload, it's still best to upload them in a single 
  
 # request. See the following for per-request limits for reference: 
  
 # https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_upload_service 
  
 conversion_adjustments 
 : 
  
 [ 
 enhancement 
 ] 
 , 
  
 # Partial failure must be set to true. 
  
 partial_failure 
 : 
  
 true 
 , 
  
 ) 
  
 # Prints any partial errors returned. 
  
 # To review the overall health of your recent uploads, see: 
  
 # https://developers.google.com/google-ads/api/docs/conversions/upload-summaries 
  
 if 
  
 response 
 . 
 partial_failure_error 
  
 puts 
  
 "Partial failure encountered: 
 #{ 
 response 
 . 
 partial_failure_error 
 . 
 message 
 } 
 ." 
  
 else 
  
 result 
  
 = 
  
 response 
 . 
 results 
 . 
 first 
  
 puts 
  
 "Uploaded conversion adjustment of 
 #{ 
 result 
 . 
 conversion_action 
 } 
 for " 
 \ 
  
 "order ID 
 #{ 
 result 
 . 
 order_id 
 } 
 ." 
  
 end 
 end 
 # Returns the result of normalizing and then hashing the string using the 
 # provided digest.  Private customer data must be hashed during upload, as 
 # described at https://support.google.com/google-ads/answer/9888656. 
 def 
  
 normalize_and_hash 
 ( 
 str 
 ) 
  
 # Remove leading and trailing whitespace and ensure all letters are lowercase 
  
 # before hasing. 
  
 Digest 
 :: 
 SHA256 
 . 
 hexdigest 
 ( 
 str 
 . 
 strip 
 . 
 downcase 
 ) 
 end 
 # Returns the result of normalizing and hashing an email address. For this use 
 # case, Google Ads requires removal of any '.' characters preceding 'gmail.com' 
 # or 'googlemail.com'. 
 def 
  
 normalize_and_hash_email 
 ( 
 email 
 ) 
  
 email_parts 
  
 = 
  
 email 
 . 
 downcase 
 . 
 split 
 ( 
 "@" 
 ) 
  
 # Removes any '.' characters from the portion of the email address before the 
  
 # domain if the domain is gmail.com or googlemail.com. 
  
 if 
  
 email_parts 
 . 
 last 
  
 =~ 
 /^(gmail|googlemail)\.com\s*/ 
  
 email_parts 
 [ 
 0 
 ] 
  
 = 
  
 email_parts 
 [ 
 0 
 ]. 
 gsub 
 ( 
 '.' 
 , 
  
 '' 
 ) 
  
 end 
  
 normalize_and_hash 
 ( 
 email_parts 
 . 
 join 
 ( 
 '@' 
 )) 
 end 
 if 
  
 __FILE__ 
  
 == 
  
 $0 
  
 options 
  
 = 
  
 {} 
  
 # The following parameter(s) should be provided to run the example. You can 
  
 # either specify these by changing the INSERT_XXX_ID_HERE values below, or on 
  
 # the command line. 
  
 # 
  
 # Parameters passed on the command line will override any parameters set in 
  
 # code. 
  
 # 
  
 # Running the example with -h will print the command line usage. 
  
 options 
 [ 
 :customer_id 
 ] 
  
 = 
  
 'INSERT_CUSTOMER_ID_HERE' 
  
 options 
 [ 
 :conversion_action_id 
 ] 
  
 = 
  
 'INSERT_CONVERSION_ACTION_ID_HERE' 
  
 options 
 [ 
 :order_id 
 ] 
  
 = 
  
 'INSERT_ORDER_ID_HERE' 
  
 OptionParser 
 . 
 new 
  
 do 
  
 | 
 opts 
 | 
  
 opts 
 . 
 banner 
  
 = 
  
 format 
 ( 
 'Usage: %s [options]' 
 , 
  
 File 
 . 
 basename 
 ( 
 __FILE__ 
 )) 
  
 opts 
 . 
 separator 
  
 '' 
  
 opts 
 . 
 separator 
  
 'Options:' 
  
 opts 
 . 
 on 
 ( 
 '-C' 
 , 
  
 '--customer-id CUSTOMER-ID' 
 , 
  
 String 
 , 
  
 'Customer ID' 
 ) 
  
 do 
  
 | 
 v 
 | 
  
 options 
 [ 
 :customer_id 
 ] 
  
 = 
  
 v 
  
 end 
  
 opts 
 . 
 on 
 ( 
 '-c' 
 , 
  
 '--conversion-action-id CONVERSION-ACTION-ID' 
 , 
  
 String 
 , 
  
 'Conversion Action ID' 
 ) 
  
 do 
  
 | 
 v 
 | 
  
 options 
 [ 
 :conversion_action_id 
 ] 
  
 = 
  
 v 
  
 end 
  
 opts 
 . 
 on 
 ( 
 '-o' 
 , 
  
 '--order-id ORDER-ID' 
 , 
  
 String 
 , 
  
 'Order ID' 
 ) 
  
 do 
  
 | 
 v 
 | 
  
 options 
 [ 
 :order_id 
 ] 
  
 = 
  
 v 
  
 end 
  
 opts 
 . 
 on 
 ( 
 '-d' 
 , 
  
 '--conversion-date-time CONVERSION-DATE-TIME' 
 , 
  
 String 
 , 
  
 'The date and time of the conversion (should be after click time).' 
 \ 
  
 ' The format is "yyyy-mm-dd hh:mm:ss+|-hh:mm", ' 
 \ 
  
 'e.g. "2019-01-01 12:32:45-08:00".' 
 ) 
  
 do 
  
 | 
 v 
 | 
  
 options 
 [ 
 :conversion_date_time 
 ] 
  
 = 
  
 v 
  
 end 
  
 opts 
 . 
 on 
 ( 
 '-u' 
 , 
  
 '--user-agent USER-AGENT' 
 , 
  
 String 
 , 
  
 'User Agent' 
 ) 
  
 do 
  
 | 
 v 
 | 
  
 options 
 [ 
 :user_agent 
 ] 
  
 = 
  
 v 
  
 end 
  
 opts 
 . 
 separator 
  
 '' 
  
 opts 
 . 
 separator 
  
 'Help:' 
  
 opts 
 . 
 on_tail 
 ( 
 '-h' 
 , 
  
 '--help' 
 , 
  
 'Show this message' 
 ) 
  
 do 
  
 puts 
  
 opts 
  
 exit 
  
 end 
  
 end 
 . 
 parse! 
  
 begin 
  
 upload_conversion_enhancement 
 ( 
  
 options 
 . 
 fetch 
 ( 
 :customer_id 
 ) 
 . 
 tr 
 ( 
 '-' 
 , 
  
 '' 
 ), 
  
 options 
 . 
 fetch 
 ( 
 :conversion_action_id 
 ), 
  
 options 
 . 
 fetch 
 ( 
 :order_id 
 ), 
  
 options 
 [ 
 :conversion_date_time 
 ] 
 , 
  
 options 
 [ 
 :user_agent 
 ] 
  
 ) 
  
 rescue 
  
 Google 
 :: 
 Ads 
 :: 
 GoogleAds 
 :: 
 Errors 
 :: 
 GoogleAdsError 
  
 = 
>  
 e 
  
 e 
 . 
 failure 
 . 
 errors 
 . 
 each 
  
 do 
  
 | 
 error 
 | 
  
 STDERR 
 . 
 printf 
 ( 
 "Error with message: %s 
 \n 
 " 
 , 
  
 error 
 . 
 message 
 ) 
  
 error 
 . 
 location 
& . 
 field_path_elements 
& . 
 each 
  
 do 
  
 | 
 field_path_element 
 | 
  
 STDERR 
 . 
 printf 
 ( 
 " 
 \t 
 On field: %s 
 \n 
 " 
 , 
  
 field_path_element 
 . 
 field_name 
 ) 
  
 end 
  
 error 
 . 
 error_code 
 . 
 to_h 
 . 
 each 
  
 do 
  
 | 
 k 
 , 
  
 v 
 | 
  
 next 
  
 if 
  
 v 
  
 == 
  
 :UNSPECIFIED 
  
 STDERR 
 . 
 printf 
 ( 
 " 
 \t 
 Type: %s 
 \n\t 
 Code: %s 
 \n 
 " 
 , 
  
 k 
 , 
  
 v 
 ) 
  
 end 
  
 end 
  
 raise 
  
 end 
 end 
  
  

Perl

 #!/usr/bin/perl -w 
 # 
 # Copyright 2021, Google LLC 
 # 
 # Licensed under the Apache License, Version 2.0 (the "License"); 
 # you may not use this file except in compliance with the License. 
 # You may obtain a copy of the License at 
 # 
 #     http://www.apache.org/licenses/LICENSE-2.0 
 # 
 # Unless required by applicable law or agreed to in writing, software 
 # distributed under the License is distributed on an "AS IS" BASIS, 
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 # See the License for the specific language governing permissions and 
 # limitations under the License. 
 # 
 # Enhances a web conversion by uploading a ConversionAdjustment containing 
 # a hashed user identifier and an order ID. 
 use 
  
 strict 
 ; 
 use 
  
 warnings 
 ; 
 use 
  
 utf8 
 ; 
 use 
  
 FindBin 
  
 qw($Bin) 
 ; 
 use 
  
 lib 
  
 "$Bin/../../lib" 
 ; 
 use 
  
 Google::Ads::GoogleAds::Client 
 ; 
 use 
  
 Google::Ads::GoogleAds::Utils::GoogleAdsHelper 
 ; 
 use 
  
 Google::Ads::GoogleAds::V21::Common::UserIdentifier 
 ; 
 use 
  
 Google::Ads::GoogleAds::V21::Common::OfflineUserAddressInfo 
 ; 
 use 
  
 Google::Ads::GoogleAds::V21::Enums::ConversionAdjustmentTypeEnum 
  
 qw(ENHANCEMENT) 
 ; 
 use 
  
 Google::Ads::GoogleAds::V21::Enums::UserIdentifierSourceEnum 
  
 qw(FIRST_PARTY) 
 ; 
 use 
  
 Google::Ads::GoogleAds::V21::Services::ConversionAdjustmentUploadService::ConversionAdjustment 
 ; 
 use 
  
 Google::Ads::GoogleAds::V21::Services::ConversionAdjustmentUploadService::GclidDateTimePair 
 ; 
 use 
  
 Google::Ads::GoogleAds::V21::Utils::ResourceNames 
 ; 
 use 
  
 Getopt::Long 
  
 qw(:config auto_help) 
 ; 
 use 
  
 Pod::Usage 
 ; 
 use 
  
 Cwd 
  
 qw(abs_path) 
 ; 
 use 
  
 Digest::SHA 
  
 qw(sha256_hex) 
 ; 
 # The following parameter(s) should be provided to run the example. You can 
 # either specify these by changing the INSERT_XXX_ID_HERE values below, or on 
 # the command line. 
 # 
 # Parameters passed on the command line will override any parameters set in 
 # code. 
 # 
 # Running the example with -h will print the command line usage. 
 my 
  
 $customer_id 
  
 = 
  
 "INSERT_CUSTOMER_ID_HERE" 
 ; 
 my 
  
 $conversion_action_id 
  
 = 
  
 "INSERT_CONVERSION_ACTION_ID_HERE" 
 ; 
 my 
  
 $order_id 
  
 = 
  
 "INSERT_ORDER_ID_HERE" 
 ; 
 # Optional: Specify the conversion date/time and user agent. 
 my 
  
 $conversion_date_time 
  
 = 
  
 undef 
 ; 
 my 
  
 $user_agent 
  
 = 
  
 undef 
 ; 
 sub 
  
 upload_enhanced_conversions_for_web 
  
 { 
  
 my 
  
 ( 
  
 $api_client 
 , 
  
 $customer_id 
 , 
  
 $conversion_action_id 
 , 
  
 $order_id 
 , 
  
 $conversion_date_time 
 , 
  
 $user_agent 
  
 ) 
  
 = 
  
 @_ 
 ; 
  
 # Construct the enhancement adjustment. 
  
 my 
  
 $enhancement 
  
 = 
  
 Google::Ads::GoogleAds::V21::Services::ConversionAdjustmentUploadService:: 
 ConversionAdjustment 
  
 - 
> new 
 ({ 
  
 adjustmentType 
  
 = 
>  
 ENHANCEMENT 
  
 }); 
  
 # Extract user email, phone, and address info from the raw data, 
  
 # normalize and hash it, then wrap it in UserIdentifier objects. 
  
 # Create a separate UserIdentifier object for each. 
  
 # The data in this example is hardcoded, but in your application 
  
 # you might read the raw data from an input file. 
  
 # 
  
 # IMPORTANT: Since the identifier attribute of UserIdentifier 
  
 # (https://developers.google.com/google-ads/api/reference/rpc/latest/UserIdentifier) 
  
 # is a oneof 
  
 # (https://protobuf.dev/programming-guides/proto3/#oneof-features), you must set 
  
 # only ONE of hashed_email, hashed_phone_number, mobile_id, third_party_user_id, 
  
 # or address-info. Setting more than one of these attributes on the same UserIdentifier 
  
 # will clear all the other members of the oneof. For example, the following code is 
  
 # INCORRECT and will result in a UserIdentifier with ONLY a hashed_phone_number: 
  
 # 
  
 # my $incorrect_user_identifier = Google::Ads::GoogleAds::V21::Common::UserIdentifier->new({ 
  
 #   hashedEmail => '...', 
  
 #   hashedPhoneNumber => '...', 
  
 # }); 
  
 my 
  
 $raw_record 
  
 = 
  
 { 
  
 # Email address that includes a period (.) before the Gmail domain. 
  
 email 
  
 = 
>  
 'alex.2@example.com' 
 , 
  
 # Address that includes all four required elements: first name, last 
  
 # name, country code, and postal code. 
  
 firstName 
  
 = 
>  
 'Alex' 
 , 
  
 lastName 
  
 = 
>  
 'Quinn' 
 , 
  
 countryCode 
  
 = 
>  
 'US' 
 , 
  
 postalCode 
  
 = 
>  
 '94045' 
 , 
  
 # Phone number to be converted to E.164 format, with a leading '+' as 
  
 # required. 
  
 phone 
  
 = 
>  
 '+1 800 5550102' 
 , 
  
 # This example lets you input conversion details as arguments, 
  
 # but in reality you might store this data alongside other user data, 
  
 # so we include it in this sample user record. 
  
 orderId 
  
 = 
>  
 $order_id 
 , 
  
 conversionActionId 
  
 = 
>  
 $conversion_action_id 
 , 
  
 conversionDateTime 
  
 = 
>  
 $conversion_date_time 
 , 
  
 currencyCode 
  
 = 
>  
 "USD" 
 , 
  
 userAgent 
  
 = 
>  
 $user_agent 
 , 
  
 }; 
  
 my 
  
 $user_identifiers 
  
 = 
  
 [] 
 ; 
  
 # Create a user identifier using the hashed email address, using the normalize 
  
 # and hash method specifically for email addresses. 
  
 my 
  
 $hashed_email 
  
 = 
  
 normalize_and_hash_email_address 
 ( 
 $raw_record 
 - 
> { 
 email 
 }); 
  
 push 
 ( 
  
 @$user_identifiers 
 , 
  
 Google::Ads::GoogleAds::V21::Common:: 
 UserIdentifier 
 - 
> new 
 ({ 
  
 hashedEmail 
  
 = 
>  
 $hashed_email 
 , 
  
 # Optional: Specify the user identifier source. 
  
 userIdentifierSource 
  
 = 
>  
 FIRST_PARTY 
  
 })); 
  
 # Check if the record has a phone number, and if so, add a UserIdentifier for it. 
  
 if 
  
 ( 
 defined 
  
 $raw_record 
 - 
> { 
 phone 
 }) 
  
 { 
  
 # Add the hashed phone number identifier to the list of UserIdentifiers. 
  
 push 
 ( 
  
 @$user_identifiers 
 , 
  
 Google::Ads::GoogleAds::V21::Common:: 
 UserIdentifier 
 - 
> new 
 ({ 
  
 hashedPhoneNumber 
  
 = 
>  
 normalize_and_hash 
 ( 
 $raw_record 
 - 
> { 
 phone 
 }, 
  
 1 
 )})); 
  
 } 
  
 # Confirm the record has all the required mailing address elements, and if so, add 
  
 # a UserIdentifier for the mailing address. 
  
 if 
  
 ( 
 defined 
  
 $raw_record 
 - 
> { 
 firstName 
 }) 
  
 { 
  
 my 
  
 $required_keys 
  
 = 
  
 [ 
 "lastName" 
 , 
  
 "countryCode" 
 , 
  
 "postalCode" 
 ]; 
  
 my 
  
 $missing_keys 
  
 = 
  
 [] 
 ; 
  
 foreach 
  
 my 
  
 $key 
  
 ( 
 @$required_keys 
 ) 
  
 { 
  
 if 
  
 ( 
 ! 
 defined 
  
 $raw_record 
 - 
> { 
 $key 
 }) 
  
 { 
  
 push 
 ( 
 @$missing_keys 
 , 
  
 $key 
 ); 
  
 } 
  
 } 
  
 if 
  
 ( 
 @$missing_keys 
 ) 
  
 { 
  
 print 
  
 "Skipping addition of mailing address information because the following" 
  
 . 
  
 "keys are missing: " 
  
 . 
  
 join 
 ( 
 "," 
 , 
  
 @$missing_keys 
 ); 
  
 } 
  
 else 
  
 { 
  
 push 
 ( 
  
 @$user_identifiers 
 , 
  
 Google::Ads::GoogleAds::V21::Common:: 
 UserIdentifier 
 - 
> new 
 ({ 
  
 addressInfo 
  
 = 
>  
 Google::Ads::GoogleAds::V21::Common:: 
 OfflineUserAddressInfo 
 - 
> new 
 ({ 
  
 # First and last name must be normalized and hashed. 
  
 hashedFirstName 
  
 = 
>  
 normalize_and_hash 
 ( 
 $raw_record 
 - 
> { 
 firstName 
 }), 
  
 hashedLastName 
  
 = 
>  
 normalize_and_hash 
 ( 
 $raw_record 
 - 
> { 
 lastName 
 }), 
  
 # Country code and zip code are sent in plain text. 
  
 countryCode 
  
 = 
>  
 $raw_record 
 - 
> { 
 countryCode 
 }, 
  
 postalCode 
  
 = 
>  
 $raw_record 
 - 
> { 
 postalCode 
 }, 
  
 })})); 
  
 } 
  
 } 
  
 # Add the user identifiers to the enhancement adjustment. 
  
 $enhancement 
 - 
> { 
 userIdentifiers 
 } 
  
 = 
  
 $user_identifiers 
 ; 
  
 # Set the conversion action. 
  
 $enhancement 
 - 
> { 
 conversionAction 
 } 
  
 = 
  
 Google::Ads::GoogleAds::V21::Utils::ResourceNames:: 
 conversion_action 
 ( 
  
 $customer_id 
 , 
  
 $raw_record 
 - 
> { 
 conversionActionId 
 }); 
  
 # Set the order ID. Enhancements MUST use order ID instead of GCLID date/time pair. 
  
 $enhancement 
 - 
> { 
 orderId 
 } 
  
 = 
  
 $raw_record 
 - 
> { 
 orderId 
 }; 
  
 # Set the conversion date and time if provided. Providing this value is optional 
  
 # but recommended. 
  
 if 
  
 ( 
 defined 
  
 $raw_record 
 - 
> { 
 conversionDateTime 
 }) 
  
 { 
  
 $enhancement 
 - 
> { 
 gclidDateTimePair 
 } 
  
 = 
  
 Google::Ads::GoogleAds::V21::Services::ConversionAdjustmentUploadService:: 
 GclidDateTimePair 
  
 - 
> new 
 ({ 
  
 conversionDateTime 
  
 = 
>  
 $raw_record 
 - 
> { 
 conversionDateTime 
 }}); 
  
 } 
  
 # Set the user agent if provided. This should match the user agent of the 
  
 # request that sent the original conversion so the conversion and its enhancement 
  
 # are either both attributed as same-device or both attributed as cross-device. 
  
 if 
  
 ( 
 defined 
  
 $raw_record 
 - 
> { 
 userAgent 
 }) 
  
 { 
  
 $enhancement 
 - 
> { 
 userAgent 
 } 
  
 = 
  
 $raw_record 
 - 
> { 
 userAgent 
 }; 
  
 } 
  
 # Upload the enhancement adjustment. Partial failure should always be set to true. 
  
 # 
  
 # NOTE: This request contains a single adjustment as a demonstration. 
  
 # However, if you have multiple adjustments to upload, it's best to 
  
 # upload multiple adjustments per request instead of sending a separate 
  
 # request per adjustment. See the following for per-request limits: 
  
 # https://developers.google.com/google-ads/api/docs/best-practices/quotas#conversion_adjustment_upload_service 
  
 my 
  
 $response 
  
 = 
  
 $api_client 
 - 
> ConversionAdjustmentUploadService 
 () 
  
 - 
> upload_conversion_adjustments 
 ({ 
  
 customerId 
  
 = 
>  
 $customer_id 
 , 
  
 conversionAdjustments 
  
 = 
>  
 [ 
 $enhancement 
 ], 
  
 # Enable partial failure (must be true). 
  
 partialFailure 
  
 = 
>  
 "true" 
  
 }); 
  
 # Print any partial errors returned. 
  
 # To review the overall health of your recent uploads, see: 
  
 # https://developers.google.com/google-ads/api/docs/conversions/upload-summaries 
  
 if 
  
 ( 
 $response 
 - 
> { 
 partialFailureError 
 }) 
  
 { 
  
 printf 
  
 "Partial error encountered: '%s'.\n" 
 , 
  
 $response 
 - 
> { 
 partialFailureError 
 }{ 
 message 
 }; 
  
 } 
  
 else 
  
 { 
  
 # Print the result. 
  
 my 
  
 $result 
  
 = 
  
 $response 
 - 
> { 
 results 
 }[ 
 0 
 ]; 
  
 printf 
  
 "Uploaded conversion adjustment of '%s' for order ID '%s'.\n" 
 , 
  
 $result 
 - 
> { 
 conversionAction 
 }, 
  
 $result 
 - 
> { 
 orderId 
 }; 
  
 } 
  
 return 
  
 1 
 ; 
 } 
 # Normalizes and hashes a string value. 
 # Private customer data must be hashed during upload, as described at 
 # https://support.google.com/google-ads/answer/7474263. 
 sub 
  
 normalize_and_hash 
  
 { 
  
 my 
  
 $value 
  
 = 
  
 shift 
 ; 
  
 my 
  
 $trim_intermediate_spaces 
  
 = 
  
 shift 
 ; 
  
 if 
  
 ( 
 $trim_intermediate_spaces 
 ) 
  
 { 
  
 $value 
  
 =~ 
  
 s/\s+//g 
 ; 
  
 } 
  
 else 
  
 { 
  
 $value 
  
 =~ 
  
 s/^\s+|\s+$//g 
 ; 
  
 } 
  
 return 
  
 sha256_hex 
 ( 
 lc 
  
 $value 
 ); 
 } 
 # Returns the result of normalizing and hashing an email address. For this use 
 # case, Google Ads requires removal of any '.' characters preceding 'gmail.com' 
 # or 'googlemail.com'. 
 sub 
  
 normalize_and_hash_email_address 
  
 { 
  
 my 
  
 $email_address 
  
 = 
  
 shift 
 ; 
  
 my 
  
 $normalized_email 
  
 = 
  
 lc 
  
 $email_address 
 ; 
  
 my 
  
 @email_parts 
  
 = 
  
 split 
 ( 
 '@' 
 , 
  
 $normalized_email 
 ); 
  
 if 
  
 ( 
 scalar 
  
 @email_parts 
 > 
 1 
 && 
 $email_parts 
 [ 
 1 
 ] 
  
 =~ 
 /^(gmail|googlemail)\.com\s*/ 
 ) 
  
 { 
  
 # Remove any '.' characters from the portion of the email address before the 
  
 # domain if the domain is 'gmail.com' or 'googlemail.com'. 
  
 $email_parts 
 [ 
 0 
 ] 
  
 =~ 
  
 s/\.//g 
 ; 
  
 $normalized_email 
  
 = 
  
 sprintf 
  
 '%s@%s' 
 , 
  
 $email_parts 
 [ 
 0 
 ], 
  
 $email_parts 
 [ 
 1 
 ]; 
  
 } 
  
 return 
  
 normalize_and_hash 
 ( 
 $normalized_email 
 , 
  
 1 
 ); 
 } 
 # Don't run the example if the file is being included. 
 if 
  
 ( 
 abs_path 
 ( 
 $0 
 ) 
  
 ne 
  
 abs_path 
 ( 
 __FILE__ 
 )) 
  
 { 
  
 return 
  
 1 
 ; 
 } 
 # Get Google Ads Client, credentials will be read from ~/googleads.properties. 
 my 
  
 $api_client 
  
 = 
  
 Google::Ads::GoogleAds:: 
 Client 
 - 
> new 
 (); 
 # By default examples are set to die on any server returned fault. 
 $api_client 
 - 
> set_die_on_faults 
 ( 
 1 
 ); 
 # Parameters passed on the command line will override any parameters set in code. 
 GetOptions 
 ( 
  
 "customer_id=s" 
  
 = 
>  
 \ 
 $customer_id 
 , 
  
 "conversion_action_id=i" 
  
 = 
>  
 \ 
 $conversion_action_id 
 , 
  
 "order_id=s" 
  
 = 
>  
 \ 
 $order_id 
 , 
  
 "conversion_date_time=s" 
  
 = 
>  
 \ 
 $conversion_date_time 
 , 
  
 "user_agent=s" 
  
 = 
>  
 \ 
 $user_agent 
 ); 
 # Print the help message if the parameters are not initialized in the code nor 
 # in the command line. 
 pod2usage 
 ( 
 2 
 ) 
  
 if 
  
 not 
  
 check_params 
 ( 
 $customer_id 
 , 
  
 $conversion_action_id 
 , 
  
 $order_id 
 ); 
 # Call the example. 
 upload_enhanced_conversions_for_web 
 ( 
 $api_client 
 , 
  
 $customer_id 
  
 =~ 
  
 s/-//g 
 r 
 , 
  
 $conversion_action_id 
 , 
  
 $order_id 
 , 
  
 $conversion_date_time 
 , 
  
 $user_agent 
 ); 
 =pod 
 =head1 NAME 
 upload_enhanced_conversions_for_web 
 =head1 DESCRIPTION 
 Adjusts an existing conversion by supplying user identifiers so Google can 
 enhance the conversion value. 
 =head1 SYNOPSIS 
 upload_enhanced_conversions_for_web.pl [options] 
 -help                       Show the help message. 
 -customer_id                The Google Ads customer ID. 
 -conversion_action_id       The conversion action ID associated with this conversion. 
 -order_id                   The unique order ID (transaction ID) of the conversion. 
 -conversion_date_time       [optional] The date time at which the conversion with the specified order ID 
 occurred. Must be after the click time, and must include the time zone offset. 
 The format is "yyyy-mm-dd hh:mm:ss+|-hh:mm", e.g. "2019-01-01 12:32:45-08:00". 
 Setting this field is optional, but recommended. 
 -user_agent                 [optional] The HTTP user agent of the conversion. 
 =cut 
  
  
Design a Mobile Site
View Site in Mobile | Classic
Share by: