Ver Fonte

Placement report

wfansh há 3 meses atrás
pai
commit
caa0143fe1

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

@@ -80,6 +80,52 @@ public class GoogleAdsController extends BaseController {
                 .build();
     }
 
+    @PostMapping("/keywordStats")
+    @ResponseBody
+    public OpenAPIResponse<Map<String, MetricsDTO>> getKeywordStats(
+            @RequestBody OpenAPIRequest<ReportRequestDTO> apiRequest)
+            throws BadRequestException, DataException {
+        long start = System.currentTimeMillis();
+        log.info("****** getKeywordStats() ****** apiRequest = {}", JsonUtils.toJson(apiRequest));
+        ReportRequestDTO reportRequest = apiRequest.getData();
+
+        // 1. Validates the request parameters.
+        if (!validateReportRequest(reportRequest)) {
+            throw new BadRequestException(apiRequest);
+        }
+
+        // 2. Executes the API request.
+        Map<String, MetricsDTO> keywordStats = googleAdsService.getKeywordStats(reportRequest);
+        log.info("****** getKeywordStats() ****** duration = {} seconds", getElapsedSeconds(start));
+        return OpenAPIResponse.<Map<String, MetricsDTO>>builder()
+                .status(APIStatus.SUCCESS)
+                .data(keywordStats)
+                .build();
+    }
+
+    @PostMapping("/placementStats")
+    @ResponseBody
+    public OpenAPIResponse<Map<String, MetricsDTO>> getPlacementStats(
+            @RequestBody OpenAPIRequest<ReportRequestDTO> apiRequest)
+            throws BadRequestException, DataException {
+        long start = System.currentTimeMillis();
+        log.info("****** getPlacementStats() ****** apiRequest = {}", JsonUtils.toJson(apiRequest));
+        ReportRequestDTO reportRequest = apiRequest.getData();
+
+        // 1. Validates the request parameters.
+        if (!validateReportRequest(reportRequest)) {
+            throw new BadRequestException(apiRequest);
+        }
+
+        // 2. Executes the API request.
+        Map<String, MetricsDTO> placementStats = googleAdsService.getPlacementStats(reportRequest);
+        log.info("****** getKeywordStats() ****** duration = {} seconds", getElapsedSeconds(start));
+        return OpenAPIResponse.<Map<String, MetricsDTO>>builder()
+                .status(APIStatus.SUCCESS)
+                .data(placementStats)
+                .build();
+    }
+
     private boolean validateReportRequest(ReportRequestDTO reportRequest) {
         return StringUtils.isNotEmpty(reportRequest.getCustomerId())
                 && StringUtils.isNotEmpty(reportRequest.getStartDate())

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

@@ -10,6 +10,7 @@ import com.wechi.adweb.bridge.exception.DataException;
 import com.wechi.adweb.bridge.google.ads.dto.CustomerStatsDTO;
 import com.wechi.adweb.bridge.google.ads.dto.MetricsDTO;
 import com.wechi.adweb.bridge.google.ads.dto.ReportRequestDTO;
+import com.wechi.adweb.bridge.google.ads.util.PlacementUtils;
 
 import lombok.extern.slf4j.Slf4j;
 
@@ -125,4 +126,85 @@ public class GoogleAdsService {
                                     LinkedHashMap::new));
         }
     }
+
+    public Map<String, MetricsDTO> getKeywordStats(ReportRequestDTO reportRequest)
+            throws DataException {
+        try (GoogleAdsServiceClient googleAdsServiceClient =
+                googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
+            String query =
+                    String.format(
+                            "SELECT ad_group_criterion.keyword.text, ad_group.name, campaign.name, "
+                                    + "metrics.impressions, metrics.clicks, metrics.ctr, metrics.average_cpc, "
+                                    + "metrics.average_cpm, metrics.conversions, metrics.cost_micros "
+                                    + "FROM keyword_view "
+                                    + "WHERE %s "
+                                    + "ORDER BY metrics.impressions DESC, metrics.clicks DESC LIMIT %d",
+                            reportRequest.toDateClause(), reportRequest.getLimit());
+
+            SearchGoogleAdsRequest request =
+                    SearchGoogleAdsRequest.newBuilder()
+                            .setCustomerId(reportRequest.getCustomerId())
+                            .setQuery(query)
+                            .build();
+
+            SearchPagedResponse response = googleAdsServiceClient.search(request);
+            return StreamSupport.stream(response.iterateAll().spliterator(), false)
+                    .collect(
+                            Collectors.toMap(
+                                    googleAdsRow ->
+                                            googleAdsRow
+                                                    .getAdGroupCriterion()
+                                                    .getKeyword()
+                                                    .getText(),
+                                    googleAdsRow ->
+                                            MetricsDTO.fromMetrics(googleAdsRow.getMetrics()),
+                                    // For duplicated keywords.
+                                    MetricsDTO::merge,
+                                    // Preserves the original order in the response.
+                                    LinkedHashMap::new));
+        }
+    }
+
+    /**
+     * Uses 'group_placement_view' instead of 'detail_placement_view', corresponding to the "WHERE
+     * ADS SHOWED" menu on the UI.
+     *
+     * <p>See https://groups.google.com/g/adwords-api/c/2sdF9iuBns0/m/6gzfhd2HAwAJ for details.
+     */
+    public Map<String, MetricsDTO> getPlacementStats(ReportRequestDTO reportRequest)
+            throws DataException {
+        try (GoogleAdsServiceClient googleAdsServiceClient =
+                googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
+            String query =
+                    String.format(
+                            "SELECT group_placement_view.display_name, group_placement_view.placement, "
+                                    + "group_placement_view.placement_type, group_placement_view.target_url, "
+                                    + "metrics.impressions, metrics.clicks, metrics.ctr, metrics.average_cpc, "
+                                    + "metrics.average_cpm, metrics.conversions, metrics.cost_micros "
+                                    + "FROM group_placement_view "
+                                    + "WHERE %s "
+                                    + "ORDER BY metrics.impressions DESC, metrics.clicks DESC LIMIT %d",
+                            reportRequest.toDateClause(), reportRequest.getLimit());
+
+            SearchGoogleAdsRequest request =
+                    SearchGoogleAdsRequest.newBuilder()
+                            .setCustomerId(reportRequest.getCustomerId())
+                            .setQuery(query)
+                            .build();
+
+            SearchPagedResponse response = googleAdsServiceClient.search(request);
+            return StreamSupport.stream(response.iterateAll().spliterator(), false)
+                    .collect(
+                            Collectors.toMap(
+                                    googleAdsRow ->
+                                            PlacementUtils.format(
+                                                    googleAdsRow.getGroupPlacementView()),
+                                    googleAdsRow ->
+                                            MetricsDTO.fromMetrics(googleAdsRow.getMetrics()),
+                                    // For duplicated placements.
+                                    MetricsDTO::merge,
+                                    // Preserves the original order in the response.
+                                    LinkedHashMap::new));
+        }
+    }
 }

+ 48 - 0
src/main/java/com/wechi/adweb/bridge/google/ads/util/PlacementUtils.java

@@ -0,0 +1,48 @@
+package com.wechi.adweb.bridge.google.ads.util;
+
+import com.google.ads.googleads.v18.resources.GroupPlacementView;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @author wfansh
+ */
+public class PlacementUtils {
+    private static final Pattern MOBILE_APP_DISPLAY_NAME_PATTERN =
+            Pattern.compile("Mobile App:(.+)\\(");
+
+    public static String format(GroupPlacementView view) {
+        return switch (view.getPlacementType()) {
+                // Website domain name.
+            case WEBSITE -> view.getDisplayName();
+            case MOBILE_APPLICATION, MOBILE_APP_CATEGORY ->
+                    formatMobileApp(view.getPlacement(), view.getDisplayName());
+            case GOOGLE_PRODUCTS -> "Google";
+            case YOUTUBE_CHANNEL, YOUTUBE_VIDEO -> "YouTube - " + view.getDisplayName();
+            default -> "UNKNOWN";
+        };
+    }
+
+    /**
+     * @param placement Represented as '1-683520774' for iOS, or '2-reas.gem.file' for Android.
+     * @param displayName 'Mobile App: Roya TV (iTunes App Store), by Watan Broadcasting Satellite'
+     *     for iOS, or 'Mobile App: Hidden Gem (Google Play), by Empiraft' for Android.
+     */
+    private static String formatMobileApp(String placement, String displayName) {
+        Matcher matcher = MOBILE_APP_DISPLAY_NAME_PATTERN.matcher(displayName);
+        if (!matcher.find()) {
+            // Returns original display name if not matched.
+            return displayName;
+        }
+        String appName = matcher.group(1).trim();
+        String platform =
+                switch (placement.charAt(0)) {
+                    case '1' -> "iOS";
+                    case '2' -> "Android";
+                    default -> "other";
+                };
+
+        return String.format("%s: %s", platform, appName);
+    }
+}