Files
AdPlatform-Server/GoogleApi/Services/AudienceService.cs
2026-03-14 13:50:09 -07:00

319 lines
16 KiB
C#

using Google.Ads.GoogleAds.Lib;
using Google.Ads.GoogleAds.V22.Services;
using Google.Ads.GoogleAds.V22.Resources;
using Google.Ads.GoogleAds.V22.Enums;
using GoogleApi.Configuration;
using GoogleApi.Models;
using Microsoft.Extensions.Options;
// Alias to avoid namespace conflicts
using GAdsServices = global::Google.Ads.GoogleAds.Services;
namespace GoogleApi.Services;
/// <summary>
/// Service for querying Google Ads audience segments and geo targets.
/// </summary>
public sealed class AudienceService
{
private readonly GoogleAdsConfig _config;
private readonly GoogleAdsClientFactory _clientFactory;
private readonly ILogger<AudienceService> _logger;
public AudienceService(
IOptions<GoogleAdsConfig> config,
GoogleAdsClientFactory clientFactory,
ILogger<AudienceService> logger)
{
_config = config.Value;
_clientFactory = clientFactory;
_logger = logger;
}
/// <summary>
/// Get all available audience segments (affinity, in-market, life events, detailed demographics)
/// </summary>
public async Task<ProviderResponse> GetAudienceSegmentsAsync(
GoogleAdsContext context,
string requestId,
CancellationToken ct)
{
_logger.LogInformation("[Audience] Fetching audience segments | RequestId={RequestId}", requestId);
if (!_clientFactory.IsRealApiEnabled || string.IsNullOrWhiteSpace(context.CustomerId))
{
return GetEmulatedAudienceSegments(requestId);
}
try
{
var client = _clientFactory.CreateClient(context);
var googleAdsService = client.GetService(GAdsServices.V22.GoogleAdsService);
var customerId = context.CustomerId;
var response = new AudienceSegmentsResponse();
// Query user interests (Affinity + In-Market)
var userInterestQuery = @"
SELECT
user_interest.user_interest_id,
user_interest.name,
user_interest.taxonomy_type,
user_interest.availabilities
FROM user_interest
WHERE user_interest.taxonomy_type IN ('AFFINITY', 'IN_MARKET')";
var userInterestResults = googleAdsService.Search(customerId, userInterestQuery);
foreach (var row in userInterestResults)
{
var ui = row.UserInterest;
var segment = new AudienceSegment
{
Id = ui.UserInterestId,
Name = ui.Name,
Type = ui.TaxonomyType.ToString()
};
if (ui.TaxonomyType == UserInterestTaxonomyTypeEnum.Types.UserInterestTaxonomyType.Affinity)
response.Affinity.Add(segment);
else if (ui.TaxonomyType == UserInterestTaxonomyTypeEnum.Types.UserInterestTaxonomyType.InMarket)
response.InMarket.Add(segment);
}
// Query life events
var lifeEventQuery = @"
SELECT
life_event.id,
life_event.name
FROM life_event";
var lifeEventResults = googleAdsService.Search(customerId, lifeEventQuery);
foreach (var row in lifeEventResults)
{
var le = row.LifeEvent;
response.LifeEvents.Add(new AudienceSegment
{
Id = le.Id,
Name = le.Name,
Type = "LIFE_EVENT"
});
}
// Query detailed demographics
var detailedDemoQuery = @"
SELECT
detailed_demographic.id,
detailed_demographic.name
FROM detailed_demographic";
var detailedDemoResults = googleAdsService.Search(customerId, detailedDemoQuery);
foreach (var row in detailedDemoResults)
{
var dd = row.DetailedDemographic;
response.DetailedDemographics.Add(new AudienceSegment
{
Id = dd.Id,
Name = dd.Name,
Type = "DETAILED_DEMOGRAPHIC"
});
}
_logger.LogInformation(
"[Audience] Retrieved {Total} segments (Affinity={Affinity}, InMarket={InMarket}, LifeEvents={Life}, Demographics={Demo}) | RequestId={RequestId}",
response.TotalCount, response.Affinity.Count, response.InMarket.Count,
response.LifeEvents.Count, response.DetailedDemographics.Count, requestId);
return ProviderResponse.Success(requestId, response);
}
catch (Exception ex)
{
_logger.LogError(ex, "[Audience] Failed to fetch segments | RequestId={RequestId}", requestId);
return ProviderResponse.Fail(requestId, "API_ERROR", ex.Message);
}
}
/// <summary>
/// Search for geo target constants by name
/// </summary>
public async Task<ProviderResponse> SearchGeoTargetsAsync(
GeoTargetSearchPayload payload,
GoogleAdsContext context,
string requestId,
CancellationToken ct)
{
_logger.LogInformation("[Audience] Searching geo targets: {Query} | RequestId={RequestId}",
payload.Query, requestId);
if (!_clientFactory.IsRealApiEnabled || string.IsNullOrWhiteSpace(context.CustomerId))
{
return GetEmulatedGeoTargets(payload.Query, requestId);
}
try
{
var client = _clientFactory.CreateClient(context);
var geoService = client.GetService(GAdsServices.V22.GeoTargetConstantService);
var request = new SuggestGeoTargetConstantsRequest
{
Locale = "en",
CountryCode = payload.CountryCode ?? "US",
LocationNames = new SuggestGeoTargetConstantsRequest.Types.LocationNames()
};
request.LocationNames.Names.Add(payload.Query);
var response = await geoService.SuggestGeoTargetConstantsAsync(request);
var results = response.GeoTargetConstantSuggestions
.Take(payload.MaxResults)
.Select(s => new GeoTarget
{
Id = s.GeoTargetConstant.Id,
Name = s.GeoTargetConstant.Name,
CanonicalName = s.GeoTargetConstant.CanonicalName,
TargetType = s.GeoTargetConstant.TargetType,
CountryCode = s.GeoTargetConstant.CountryCode,
ParentGeoTarget = s.GeoTargetConstant.ParentGeoTarget
})
.ToList();
_logger.LogInformation("[Audience] Found {Count} geo targets for '{Query}' | RequestId={RequestId}",
results.Count, payload.Query, requestId);
return ProviderResponse.Success(requestId, new GeoTargetSearchResponse
{
Query = payload.Query,
Results = results
});
}
catch (Exception ex)
{
_logger.LogError(ex, "[Audience] Failed to search geo targets | RequestId={RequestId}", requestId);
return ProviderResponse.Fail(requestId, "API_ERROR", ex.Message);
}
}
#region Emulated Responses
private ProviderResponse GetEmulatedAudienceSegments(string requestId)
{
_logger.LogInformation("[Audience] Returning emulated audience segments | RequestId={RequestId}", requestId);
var response = new AudienceSegmentsResponse
{
Affinity = new List<AudienceSegment>
{
new() { Id = 80001, Name = "Sports & Fitness/Sports Fans", Type = "AFFINITY" },
new() { Id = 80002, Name = "Sports & Fitness/Health & Fitness Buffs", Type = "AFFINITY" },
new() { Id = 80003, Name = "Technology/Technophiles", Type = "AFFINITY" },
new() { Id = 80004, Name = "Travel/Travel Buffs", Type = "AFFINITY" },
new() { Id = 80005, Name = "Food & Dining/Foodies", Type = "AFFINITY" },
new() { Id = 80006, Name = "Home & Garden/Home Decor Enthusiasts", Type = "AFFINITY" },
new() { Id = 80007, Name = "Media & Entertainment/Movie Lovers", Type = "AFFINITY" },
new() { Id = 80008, Name = "Media & Entertainment/Music Lovers", Type = "AFFINITY" },
new() { Id = 80009, Name = "Shoppers/Value Shoppers", Type = "AFFINITY" },
new() { Id = 80010, Name = "Shoppers/Luxury Shoppers", Type = "AFFINITY" },
new() { Id = 80011, Name = "Lifestyles & Hobbies/Pet Lovers", Type = "AFFINITY" },
new() { Id = 80012, Name = "Lifestyles & Hobbies/Outdoor Enthusiasts", Type = "AFFINITY" },
new() { Id = 80013, Name = "News & Politics/Avid News Readers", Type = "AFFINITY" },
new() { Id = 80014, Name = "Beauty & Wellness/Beauty Mavens", Type = "AFFINITY" },
new() { Id = 80015, Name = "Vehicles & Transportation/Auto Enthusiasts", Type = "AFFINITY" },
},
InMarket = new List<AudienceSegment>
{
new() { Id = 90001, Name = "Apparel & Accessories/Athletic Apparel", Type = "IN_MARKET" },
new() { Id = 90002, Name = "Autos & Vehicles/Motor Vehicles (New)", Type = "IN_MARKET" },
new() { Id = 90003, Name = "Autos & Vehicles/Motor Vehicles (Used)", Type = "IN_MARKET" },
new() { Id = 90004, Name = "Business Services/Advertising & Marketing Services", Type = "IN_MARKET" },
new() { Id = 90005, Name = "Consumer Electronics/Computers & Peripherals", Type = "IN_MARKET" },
new() { Id = 90006, Name = "Consumer Electronics/Mobile Phones", Type = "IN_MARKET" },
new() { Id = 90007, Name = "Education/Primary & Secondary Schools (K-12)", Type = "IN_MARKET" },
new() { Id = 90008, Name = "Employment/Jobs", Type = "IN_MARKET" },
new() { Id = 90009, Name = "Financial Services/Insurance/Auto Insurance", Type = "IN_MARKET" },
new() { Id = 90010, Name = "Financial Services/Investment Services", Type = "IN_MARKET" },
new() { Id = 90011, Name = "Home & Garden/Home Improvement", Type = "IN_MARKET" },
new() { Id = 90012, Name = "Real Estate/Residential Properties", Type = "IN_MARKET" },
new() { Id = 90013, Name = "Software/Business Software", Type = "IN_MARKET" },
new() { Id = 90014, Name = "Travel/Hotels & Accommodations", Type = "IN_MARKET" },
new() { Id = 90015, Name = "Travel/Air Travel", Type = "IN_MARKET" },
},
LifeEvents = new List<AudienceSegment>
{
new() { Id = 70001, Name = "About to graduate college", Type = "LIFE_EVENT" },
new() { Id = 70002, Name = "Getting married soon", Type = "LIFE_EVENT" },
new() { Id = 70003, Name = "Recently married", Type = "LIFE_EVENT" },
new() { Id = 70004, Name = "Moving soon", Type = "LIFE_EVENT" },
new() { Id = 70005, Name = "Recently moved", Type = "LIFE_EVENT" },
new() { Id = 70006, Name = "Purchasing a home soon", Type = "LIFE_EVENT" },
new() { Id = 70007, Name = "Recently purchased a home", Type = "LIFE_EVENT" },
new() { Id = 70008, Name = "Starting a new job", Type = "LIFE_EVENT" },
new() { Id = 70009, Name = "Retiring soon", Type = "LIFE_EVENT" },
new() { Id = 70010, Name = "Recently started a business", Type = "LIFE_EVENT" },
},
DetailedDemographics = new List<AudienceSegment>
{
new() { Id = 60001, Name = "Parental status/Parents/Parents of infants (0-1 years)", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60002, Name = "Parental status/Parents/Parents of toddlers (1-3 years)", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60003, Name = "Parental status/Parents/Parents of preschoolers (3-5 years)", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60004, Name = "Parental status/Parents/Parents of grade schoolers (6-12 years)", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60005, Name = "Parental status/Parents/Parents of teens (13-17 years)", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60006, Name = "Marital status/Single", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60007, Name = "Marital status/In a relationship", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60008, Name = "Marital status/Married", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60009, Name = "Education/Current college students", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60010, Name = "Education/Bachelor's degree", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60011, Name = "Education/Advanced degree", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60012, Name = "Homeownership/Homeowners", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60013, Name = "Homeownership/Renters", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60014, Name = "Employment/Industry/Technology", Type = "DETAILED_DEMOGRAPHIC" },
new() { Id = 60015, Name = "Employment/Company size/Large employers (10,000+)", Type = "DETAILED_DEMOGRAPHIC" },
},
RetrievedAt = DateTimeOffset.UtcNow
};
return ProviderResponse.Success(requestId, response);
}
private ProviderResponse GetEmulatedGeoTargets(string query, string requestId)
{
_logger.LogInformation("[Audience] Returning emulated geo targets for '{Query}' | RequestId={RequestId}",
query, requestId);
var allTargets = new List<GeoTarget>
{
new() { Id = 9061285, Name = "Huntington Beach", CanonicalName = "Huntington Beach,California,United States", TargetType = "City", CountryCode = "US" },
new() { Id = 1013962, Name = "Orange County", CanonicalName = "Orange County,California,United States", TargetType = "County", CountryCode = "US" },
new() { Id = 21137, Name = "California", CanonicalName = "California,United States", TargetType = "State", CountryCode = "US" },
new() { Id = 1014221, Name = "Los Angeles", CanonicalName = "Los Angeles,California,United States", TargetType = "City", CountryCode = "US" },
new() { Id = 1014218, Name = "Los Angeles County", CanonicalName = "Los Angeles County,California,United States", TargetType = "County", CountryCode = "US" },
new() { Id = 9031936, Name = "Irvine", CanonicalName = "Irvine,California,United States", TargetType = "City", CountryCode = "US" },
new() { Id = 9031935, Name = "Costa Mesa", CanonicalName = "Costa Mesa,California,United States", TargetType = "City", CountryCode = "US" },
new() { Id = 9031938, Name = "Newport Beach", CanonicalName = "Newport Beach,California,United States", TargetType = "City", CountryCode = "US" },
new() { Id = 9031937, Name = "Santa Ana", CanonicalName = "Santa Ana,California,United States", TargetType = "City", CountryCode = "US" },
new() { Id = 1023191, Name = "New York", CanonicalName = "New York,New York,United States", TargetType = "City", CountryCode = "US" },
new() { Id = 21167, Name = "New York", CanonicalName = "New York,United States", TargetType = "State", CountryCode = "US" },
new() { Id = 1014895, Name = "Chicago", CanonicalName = "Chicago,Illinois,United States", TargetType = "City", CountryCode = "US" },
new() { Id = 2840, Name = "United States", CanonicalName = "United States", TargetType = "Country", CountryCode = "US" },
};
// Simple filter by query
var queryLower = query.ToLowerInvariant();
var matches = allTargets
.Where(t => t.Name.ToLowerInvariant().Contains(queryLower) ||
t.CanonicalName.ToLowerInvariant().Contains(queryLower))
.Take(10)
.ToList();
return ProviderResponse.Success(requestId, new GeoTargetSearchResponse
{
Query = query,
Results = matches
});
}
#endregion
}