319 lines
16 KiB
C#
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
|
|
}
|