Browse Source

Google ads service

wfansh 3 months ago
parent
commit
6367697c40

+ 1 - 0
build.gradle

@@ -36,6 +36,7 @@ dependencies {
     implementation 'com.google.apis:google-api-services-tagmanager:v2-rev20240701-2.0.0'
     implementation 'com.google.analytics:google-analytics-admin:0.59.0'
     implementation 'com.google.analytics:google-analytics-data:0.60.0'
+    implementation 'com.google.api-ads:google-ads:34.0.0'
 
     testImplementation 'org.springframework.boot:spring-boot-starter-test'
     testImplementation 'org.junit.jupiter:junit-jupiter-api'

+ 55 - 0
src/main/java/com/wechi/adweb/bridge/google/ads/controller/GoogleAdsController.java

@@ -0,0 +1,55 @@
+package com.wechi.adweb.bridge.google.ads.controller;
+
+import com.wechi.adweb.bridge.common.APIStatus;
+import com.wechi.adweb.bridge.common.BaseController;
+import com.wechi.adweb.bridge.common.OpenAPIRequest;
+import com.wechi.adweb.bridge.common.OpenAPIResponse;
+import com.wechi.adweb.bridge.exception.BadRequestException;
+import com.wechi.adweb.bridge.exception.DataException;
+import com.wechi.adweb.bridge.google.ads.dto.CustomerStatsDTO;
+import com.wechi.adweb.bridge.google.ads.service.GoogleAdsService;
+import com.wechi.adweb.bridge.util.JsonUtils;
+
+import io.swagger.v3.oas.annotations.Operation;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * @author wfansh
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/google/ads")
+public class GoogleAdsController extends BaseController {
+
+    @Autowired private GoogleAdsService googleAdsService;
+
+    @Operation(summary = "Get customer stats by customer ID.")
+    @PostMapping("/customerStats")
+    @ResponseBody
+    public OpenAPIResponse<CustomerStatsDTO> getCustomerStats(
+            @RequestBody OpenAPIRequest<String> apiRequest)
+            throws BadRequestException, DataException {
+        long start = System.currentTimeMillis();
+        log.info("****** getCustomerStats() ****** apiRequest = {}", JsonUtils.toJson(apiRequest));
+        String customerId = apiRequest.getData();
+
+        // 1. Validates the request parameters.
+        if (StringUtils.isEmpty(customerId)) {
+            throw new BadRequestException(apiRequest);
+        }
+
+        // 2. Executes the API request.
+        CustomerStatsDTO customerStats = googleAdsService.getCustomerStats(customerId);
+        log.info(
+                "****** getCustomerStats() ****** duration = {} seconds", getElapsedSeconds(start));
+        return OpenAPIResponse.<CustomerStatsDTO>builder()
+                .status(APIStatus.SUCCESS)
+                .data(customerStats)
+                .build();
+    }
+}

+ 21 - 0
src/main/java/com/wechi/adweb/bridge/google/ads/dto/CustomerStatsDTO.java

@@ -0,0 +1,21 @@
+package com.wechi.adweb.bridge.google.ads.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * @author wfansh
+ */
+@Data
+@Builder
+public class CustomerStatsDTO {
+
+    private String descriptiveName;
+    private String currencyCode;
+
+    private long balanceMicros;
+    private long yesterdayCostMicros;
+
+    // See https://developers.google.com/google-ads/api/docs/recommendations.
+    private double optimizationScore;
+}

+ 51 - 0
src/main/java/com/wechi/adweb/bridge/google/ads/dto/MetricsDTO.java

@@ -0,0 +1,51 @@
+package com.wechi.adweb.bridge.google.ads.dto;
+
+import com.google.ads.googleads.v18.common.Metrics;
+
+import lombok.Data;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * @author wfansh
+ */
+@Data
+@SuperBuilder
+public class MetricsDTO {
+    private long impressions;
+    private long clicks;
+
+    private double ctr;
+    private double averageCpc;
+    private double averageCpm;
+
+    private double conversions;
+
+    private long costMicros;
+
+    public MetricsDTO merge(MetricsDTO metricsDTO) {
+        this.impressions += metricsDTO.impressions;
+        this.clicks += metricsDTO.clicks;
+
+        this.ctr += metricsDTO.ctr;
+        this.averageCpc += metricsDTO.averageCpc;
+        this.averageCpm += metricsDTO.averageCpm;
+
+        this.conversions += metricsDTO.conversions;
+
+        this.costMicros += metricsDTO.costMicros;
+
+        return this;
+    }
+
+    public static MetricsDTO fromMetrics(Metrics metrics) {
+        return MetricsDTO.builder()
+                .impressions(metrics.getImpressions())
+                .clicks(metrics.getClicks())
+                .ctr(metrics.getCtr())
+                .averageCpc(metrics.getAverageCpc())
+                .averageCpm(metrics.getAverageCpm())
+                .conversions(metrics.getConversions())
+                .costMicros(metrics.getCostMicros())
+                .build();
+    }
+}

+ 45 - 0
src/main/java/com/wechi/adweb/bridge/google/ads/service/GoogleAdsConfig.java

@@ -0,0 +1,45 @@
+package com.wechi.adweb.bridge.google.ads.service;
+
+import com.google.ads.googleads.lib.GoogleAdsClient;
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.UserCredentials;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author wfansh
+ */
+@Configuration
+public class GoogleAdsConfig {
+    @Value("${api.googleads.clientId}")
+    private String clientId;
+
+    @Value("${api.googleads.clientSecret}")
+    private String clientSecret;
+
+    @Value("${api.googleads.refreshToken}")
+    private String refreshToken;
+
+    @Value("${api.googleads.developerToken}")
+    private String developerToken;
+
+    @Value("${api.googleads.loginCustomerId}")
+    private Long loginCustomerId;
+
+    @Bean
+    public GoogleAdsClient googleAdsClient() {
+        Credentials credentials =
+                UserCredentials.newBuilder()
+                        .setClientId(clientId)
+                        .setClientSecret(clientSecret)
+                        .setRefreshToken(refreshToken)
+                        .build();
+        return GoogleAdsClient.newBuilder()
+                .setCredentials(credentials)
+                .setDeveloperToken(developerToken)
+                .setLoginCustomerId(loginCustomerId)
+                .build();
+    }
+}

+ 90 - 0
src/main/java/com/wechi/adweb/bridge/google/ads/service/GoogleAdsService.java

@@ -0,0 +1,90 @@
+package com.wechi.adweb.bridge.google.ads.service;
+
+import com.google.ads.googleads.lib.GoogleAdsClient;
+import com.google.ads.googleads.v18.services.GoogleAdsRow;
+import com.google.ads.googleads.v18.services.GoogleAdsServiceClient;
+import com.google.ads.googleads.v18.services.GoogleAdsServiceClient.SearchPagedResponse;
+import com.google.ads.googleads.v18.services.SearchGoogleAdsRequest;
+import com.google.api.core.ApiFuture;
+import com.wechi.adweb.bridge.exception.DataException;
+import com.wechi.adweb.bridge.google.ads.dto.CustomerStatsDTO;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.StreamSupport;
+
+/**
+ * @author wfansh
+ */
+@Slf4j
+@Service
+public class GoogleAdsService {
+
+    @Autowired private GoogleAdsClient googleAdsClient;
+
+    public CustomerStatsDTO getCustomerStats(String customerId) throws DataException {
+        try (GoogleAdsServiceClient googleAdsServiceClient =
+                googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
+
+            // 1. Queries customer resource fields, yesterday's cost and optimization score.
+            String customerQuery =
+                    "SELECT customer.descriptive_name, customer.currency_code, customer.optimization_score, "
+                            + "metrics.cost_micros "
+                            + "FROM customer WHERE segments.date DURING YESTERDAY";
+            ApiFuture<SearchPagedResponse> customerResponse =
+                    googleAdsServiceClient
+                            .searchPagedCallable()
+                            .futureCall(
+                                    SearchGoogleAdsRequest.newBuilder()
+                                            .setCustomerId(customerId)
+                                            .setQuery(customerQuery)
+                                            .build());
+
+            // 2. Queries customer balance via account budgets.
+            String balanceQuery =
+                    String.format(
+                            "SELECT account_budget.adjusted_spending_limit_micros, account_budget.total_adjustments_micros, "
+                                    + "account_budget.amount_served_micros "
+                                    + "FROM account_budget "
+                                    + "WHERE account_budget.status = 'APPROVED' AND account_budget.approved_end_date_time >= '%s'",
+                            LocalDate.now());
+            ApiFuture<SearchPagedResponse> balanceResponse =
+                    googleAdsServiceClient
+                            .searchPagedCallable()
+                            .futureCall(
+                                    SearchGoogleAdsRequest.newBuilder()
+                                            .setCustomerId(customerId)
+                                            .setQuery(balanceQuery)
+                                            .build());
+
+            // Waits for the completion of the above ApiFuture calls, using get() method.
+            GoogleAdsRow customerResult = customerResponse.get().iterateAll().iterator().next();
+            long balanceMicros =
+                    StreamSupport.stream(balanceResponse.get().iterateAll().spliterator(), false)
+                            .map(GoogleAdsRow::getAccountBudget)
+                            .mapToLong(
+                                    accountBudget ->
+                                            accountBudget.getAdjustedSpendingLimitMicros()
+                                                    - accountBudget.getAmountServedMicros())
+                            .sum();
+
+            return CustomerStatsDTO.builder()
+                    .descriptiveName(customerResult.getCustomer().getDescriptiveName())
+                    .currencyCode(customerResult.getCustomer().getCurrencyCode())
+                    // Balance.
+                    .balanceMicros(balanceMicros)
+                    .yesterdayCostMicros(customerResult.getMetrics().getCostMicros())
+                    // Optimization score.
+                    .optimizationScore(customerResult.getCustomer().getOptimizationScore())
+                    .build();
+        } catch (InterruptedException | ExecutionException e) {
+            log.error(e.getMessage());
+            throw new DataException(e);
+        }
+    }
+}

+ 8 - 2
src/main/resources/application.properties

@@ -1,8 +1,14 @@
 spring.application.name=adweb3-data-bridge
 ## Spring Boot
 server.port=9002
-## Google Services
+## Google Analytics and Google Tag Manager
 google.service.account.key=google/service-account-key.json
 google.gtm.container.admins=advich456@gmail.com,wfansh@gmail.com
 google.gtm.snippet.head.template=google/gtm/head-snippet.tmpl
-google.gtm.snippet.body.template=google/gtm/body-snippet.tmpl
+google.gtm.snippet.body.template=google/gtm/body-snippet.tmpl
+## Google Ads
+api.googleads.clientId=560637910926-nlhhhc9csdttcg7bg9djl92c84r6d12k.apps.googleusercontent.com
+api.googleads.clientSecret=cT8GGLXa0G8RZJM2rnXx7V_G
+api.googleads.refreshToken=1//0eux9A808FhSzCgYIARAAGA4SNwF-L9IrTOTgsi4N3Yyk4oczqbhxNoJx2LBzpLlGeT7oU9xfqKwSKYJ6GU8bwuYbkvKrI8sUV-s
+api.googleads.developerToken=p0z9bFoLMEBSgnxzv5EtuA
+api.googleads.loginCustomerId=1035079252

+ 27 - 0
src/test/java/com/wechi/adweb/bridge/google/ads/GoogleAdsServiceTest.java

@@ -0,0 +1,27 @@
+package com.wechi.adweb.bridge.google.ads;
+
+import com.wechi.adweb.bridge.exception.DataException;
+import com.wechi.adweb.bridge.google.ads.dto.CustomerStatsDTO;
+import com.wechi.adweb.bridge.google.ads.service.GoogleAdsService;
+import com.wechi.adweb.bridge.util.JsonUtils;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author wfansh
+ */
+@SpringBootTest
+public class GoogleAdsServiceTest {
+
+    @Autowired private GoogleAdsService googleAdsService;
+
+    private final String customerId = "7703736321";
+
+    @Test
+    void testGetCustomerStats() throws DataException {
+        CustomerStatsDTO customerStats = googleAdsService.getCustomerStats(customerId);
+        System.out.println(JsonUtils.toJson(customerStats));
+    }
+}

+ 3 - 1
src/test/java/com/wechi/adweb/bridge/google/analytics/service/GAAdminServiceTests.java

@@ -10,6 +10,9 @@ import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 
+/**
+ * @author wfansh
+ */
 @SpringBootTest
 public class GAAdminServiceTests {
 
@@ -20,7 +23,6 @@ public class GAAdminServiceTests {
     @Test
     @Disabled("Disabled due to operations on the production account.")
     void testCreateProperty() throws DataException {
-
         GAPropertyDTO gaProperty =
                 gaAdminService.createPropertyWithDataStream(
                         accountResourceName,