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