Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Core: Configure analytics adapters per account #3443

Merged
merged 11 commits into from
Sep 24, 2024
1 change: 1 addition & 0 deletions docs/application-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Keep in mind following restrictions:
- `analytics.allow-client-details` - when true, this boolean setting allows responses to transmit the server-side analytics tags to support client-side analytics adapters. Defaults to false.
- `analytics.auction-events.<channel>` - defines which channels are supported by analytics for this account
- `analytics.modules.<module-name>.*` - space for `module-name` analytics module specific configuration, may be of any shape
- `analytics.modules.<analytic-adapter-name>.*` - a space for specific data for the analytics adapter, which may include an enabled property to control whether the adapter should be triggered, along with other adapter-specific properties. These will be merged under `ext.prebid.analytics.<analytic-adapter-name>` in the request.
- `metrics.verbosity-level` - defines verbosity level of metrics for this account, overrides `metrics.accounts` application settings configuration.
- `cookie-sync.default-limit` - if the "limit" isn't specified in the `/cookie_sync` request, this is what to use
- `cookie-sync.max-limit` - if the "limit" is specified in the `/cookie_sync` request, it can't be greater than this
Expand Down
1 change: 1 addition & 0 deletions docs/config-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ If not defined in config all other Health Checkers would be disabled and endpoin
- `geolocation.maxmind.remote-file-syncer` - use RemoteFileSyncer component for downloading/updating MaxMind database file. See [RemoteFileSyncer](#remote-file-syncer) section for its configuration.

## Analytics
- `analytics.global.adapters` - Names of analytics adapters that will work for each request, except those disabled at the account level.
- `analytics.pubstack.enabled` - if equals to `true` the Pubstack analytics module will be enabled. Default value is `false`.
- `analytics.pubstack.endpoint` - url for reporting events and fetching configuration.
- `analytics.pubstack.scopeid` - defined the scope provided by the Pubstack Support Team.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/**
* Represents a transaction at /openrtb2/amp endpoint.
*/
@Builder
@Builder(toBuilder = true)
@Value
public class AmpEvent {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.prebid.server.auction.privacy.enforcement.TcfEnforcement;
import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
import org.prebid.server.exception.InvalidRequestException;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.log.ConditionalLogger;
import org.prebid.server.log.Logger;
import org.prebid.server.log.LoggerFactory;
Expand All @@ -37,20 +38,20 @@
import org.prebid.server.privacy.gdpr.model.TcfContext;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
import org.prebid.server.settings.model.Account;
import org.prebid.server.settings.model.AccountAnalyticsConfig;
import org.prebid.server.util.StreamUtil;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;

/**
* Class dispatches event processing to all enabled reporters.
*/
public class AnalyticsReporterDelegator {

private static final Logger logger = LoggerFactory.getLogger(AnalyticsReporterDelegator.class);
Expand All @@ -63,6 +64,8 @@ public class AnalyticsReporterDelegator {
private final UserFpdActivityMask mask;
private final Metrics metrics;
private final double logSamplingRate;
private final Set<String> globalEnabledAdapters;
private final JacksonMapper mapper;

private final Set<Integer> reporterVendorIds;
private final Set<String> reporterNames;
Expand All @@ -72,14 +75,20 @@ public AnalyticsReporterDelegator(Vertx vertx,
TcfEnforcement tcfEnforcement,
UserFpdActivityMask userFpdActivityMask,
Metrics metrics,
double logSamplingRate) {
double logSamplingRate,
Set<String> globalEnabledAdapters,
JacksonMapper mapper) {

this.vertx = Objects.requireNonNull(vertx);
this.delegates = Objects.requireNonNull(delegates);
this.tcfEnforcement = Objects.requireNonNull(tcfEnforcement);
this.mask = Objects.requireNonNull(userFpdActivityMask);
this.metrics = Objects.requireNonNull(metrics);
this.logSamplingRate = logSamplingRate;
this.globalEnabledAdapters = CollectionUtils.isEmpty(globalEnabledAdapters)
? Collections.emptySet()
: globalEnabledAdapters;
this.mapper = Objects.requireNonNull(mapper);

reporterVendorIds = delegates.stream().map(AnalyticsReporter::vendorId).collect(Collectors.toSet());
reporterNames = delegates.stream().map(AnalyticsReporter::name).collect(Collectors.toSet());
Expand Down Expand Up @@ -163,11 +172,14 @@ private static boolean isNotEmptyObjectNode(JsonNode analytics) {
return analytics != null && analytics.isObject() && !analytics.isEmpty();
}

private static <T> boolean isAllowedAdapter(T event, String adapter) {
private <T> boolean isAllowedAdapter(T event, String adapter) {
final ActivityInfrastructure activityInfrastructure;
final ActivityInvocationPayload activityInvocationPayload;
switch (event) {
case AuctionEvent auctionEvent -> {
if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, auctionEvent.getAuctionContext())) {
return false;
}
final AuctionContext auctionContext = auctionEvent.getAuctionContext();
activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null;
activityInvocationPayload = auctionContext != null
Expand All @@ -177,6 +189,10 @@ private static <T> boolean isAllowedAdapter(T event, String adapter) {
: null;
}
case AmpEvent ampEvent -> {
if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, ampEvent.getAuctionContext())) {
return false;
}

final AuctionContext auctionContext = ampEvent.getAuctionContext();
activityInfrastructure = auctionContext != null ? auctionContext.getActivityInfrastructure() : null;
activityInvocationPayload = auctionContext != null
Expand All @@ -186,9 +202,19 @@ private static <T> boolean isAllowedAdapter(T event, String adapter) {
: null;
}
case NotificationEvent notificationEvent -> {
if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, notificationEvent.getAccount())) {
return false;
}
activityInfrastructure = notificationEvent.getActivityInfrastructure();
activityInvocationPayload = activityInvocationPayload(adapter);
}
case VideoEvent videoEvent -> {
if (isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter, videoEvent.getAuctionContext())) {
return false;
}
activityInfrastructure = null;
activityInvocationPayload = null;
}
case null, default -> {
activityInfrastructure = null;
activityInvocationPayload = null;
Expand All @@ -198,6 +224,28 @@ private static <T> boolean isAllowedAdapter(T event, String adapter) {
return isAllowedActivity(activityInfrastructure, Activity.REPORT_ANALYTICS, activityInvocationPayload);
}

private boolean isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(String adapter, AuctionContext auctionContext) {
return isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(adapter,
Optional.ofNullable(auctionContext)
.map(AuctionContext::getAccount)
.orElse(null));
}

private boolean isNotAllowedAdapterByGlobalOrAccountAnalyticsConfig(String adapter, Account account) {
final Map<String, ObjectNode> modules = Optional.ofNullable(account)
.map(Account::getAnalytics)
.map(AccountAnalyticsConfig::getModules)
.orElse(null);

if (modules != null && modules.containsKey(adapter)) {
final ObjectNode moduleConfig = modules.get(adapter);
return moduleConfig == null || !moduleConfig.has("enabled")
|| !moduleConfig.get("enabled").asBoolean();
}

return !globalEnabledAdapters.contains(adapter);
}

private static ActivityInvocationPayload activityInvocationPayload(String adapterName) {
return ActivityInvocationPayloadImpl.of(ComponentType.ANALYTICS, adapterName);
}
Expand Down Expand Up @@ -299,7 +347,8 @@ private static ObjectNode prepareAnalytics(ObjectNode analytics, String adapterN

private <T> void processEventByReporter(AnalyticsReporter analyticsReporter, T event) {
final String reporterName = analyticsReporter.name();
analyticsReporter.processEvent(event)

analyticsReporter.processEvent(updateEventIfRequired(event, analyticsReporter.name()))
.map(ignored -> processSuccess(event, reporterName))
.otherwise(exception -> processFail(exception, event, reporterName));
}
Expand Down Expand Up @@ -335,4 +384,89 @@ private <T> void updateMetricsByEventType(T event, String analyticsCode, MetricN

metrics.updateAnalyticEventMetric(analyticsCode, eventType, result);
}

private <T> T updateEventIfRequired(T event, String adapter) {
switch (event) {
case AuctionEvent auctionEvent -> {
final AuctionContext auctionContext = updateAuctionContext(auctionEvent.getAuctionContext(), adapter);
return auctionContext != null
? (T) auctionEvent.toBuilder().auctionContext(auctionContext).build()
: event;
}
case AmpEvent ampEvent -> {
final AuctionContext auctionContext = updateAuctionContext(ampEvent.getAuctionContext(), adapter);
return auctionContext != null
? (T) ampEvent.toBuilder().auctionContext(auctionContext).build()
: event;
}
case VideoEvent videoEvent -> {
final AuctionContext auctionContext = updateAuctionContext(videoEvent.getAuctionContext(), adapter);
return auctionContext != null
? (T) videoEvent.toBuilder().auctionContext(auctionContext).build()
: event;
}
case null, default -> {
return event;
}
}
}

private AuctionContext updateAuctionContext(AuctionContext context, String adapterName) {
final Map<String, ObjectNode> modules = Optional.ofNullable(context)
.map(AuctionContext::getAccount)
.map(Account::getAnalytics)
.map(AccountAnalyticsConfig::getModules)
.orElse(null);

if (modules != null && modules.containsKey(adapterName)) {
final ObjectNode moduleConfig = modules.get(adapterName);
if (moduleConfigContainsAdapterSpecificData(moduleConfig)) {
final JsonNode analyticsNode = Optional.ofNullable(context.getBidRequest())
.map(BidRequest::getExt)
.map(ExtRequest::getPrebid)
.map(ExtRequestPrebid::getAnalytics)
.orElse(null);

if (analyticsNode != null && analyticsNode.isObject()) {
final ObjectNode adapterNode = Optional.ofNullable((ObjectNode) analyticsNode.get(adapterName))
.orElse(mapper.mapper().createObjectNode());

moduleConfig.fields().forEachRemaining(entry -> {
final String fieldName = entry.getKey();
if (!"enabled".equals(fieldName) && !adapterNode.has(fieldName)) {
adapterNode.set(fieldName, entry.getValue());
}
});

((ObjectNode) analyticsNode).set(adapterName, adapterNode);
final ExtRequestPrebid updatedPrebid = ExtRequestPrebid.builder()
.analytics(analyticsNode)
.build();
final ExtRequest updatedExtRequest = ExtRequest.of(updatedPrebid);
final BidRequest updatedBidRequest = context.getBidRequest().toBuilder()
.ext(updatedExtRequest)
.build();
return context.toBuilder()
.bidRequest(updatedBidRequest)
.build();
}
}
}

return null;
}

private boolean moduleConfigContainsAdapterSpecificData(ObjectNode moduleConfig) {
if (moduleConfig != null) {
final Iterator<String> fieldNames = moduleConfig.fieldNames();
while (fieldNames.hasNext()) {
final String fieldName = fieldNames.next();
if (!"enabled".equals(fieldName)) {
return true;
}
}
}

return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import jakarta.validation.constraints.NotNull;
import java.time.Clock;
import java.util.List;
import java.util.Set;
import java.util.Map;
import java.util.stream.Collectors;

Expand All @@ -45,15 +46,19 @@ AnalyticsReporterDelegator analyticsReporterDelegator(
TcfEnforcement tcfEnforcement,
UserFpdActivityMask userFpdActivityMask,
Metrics metrics,
@Value("${logging.sampling-rate:0.01}") double logSamplingRate) {
@Value("${logging.sampling-rate:0.01}") double logSamplingRate,
@Value("${analytics.global.adapters}") Set<String> globalEnabledAdapters,
JacksonMapper mapper) {

return new AnalyticsReporterDelegator(
vertx,
ListUtils.emptyIfNull(delegates),
tcfEnforcement,
userFpdActivityMask,
metrics,
logSamplingRate);
logSamplingRate,
globalEnabledAdapters,
mapper);
}

@Bean
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ ipv6:
anon-left-mask-bits: 56
private-networks: ::1/128, 2001:db8::/32, fc00::/7, fe80::/10, ff00::/8
analytics:
global:
adapters: logAnalytics, pubstack, greenbids, agmaAnalytics
pubstack:
enabled: false
endpoint: http://localhost:8090
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class AccountAnalyticsConfig {

Map<String, Boolean> auctionEvents
Boolean allowClientDetails
AnalyticsModule modules

@JsonProperty("auction_events")
Map<String, Boolean> auctionEventsSnakeCase
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.prebid.server.functional.model.config

import groovy.transform.ToString

@ToString(includeNames = true, ignoreNulls = true)
class AnalyticsModule {

LogAnalytics logAnalytics
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.prebid.server.functional.model.config

import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming
import groovy.transform.ToString

@ToString(includeNames = true, ignoreNulls = true)
@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy)
class LogAnalytics {

Boolean enabled
String additionalData
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package org.prebid.server.functional.model.request.auction

import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming
import groovy.transform.ToString
import org.prebid.server.functional.model.config.LogAnalytics

@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy)
@ToString(includeNames = true, ignoreNulls = true)
class PrebidAnalytics {

AnalyticsOptions options
LogAnalytics logAnalytics
}
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,21 @@ class PrebidServerService implements ObjectMapperWrapper {
filteredLogs
}

String getLogsByValue(String value) {
if (!value) {
throw new IllegalArgumentException("Value is null or empty")
}
getPbsLogsByValue(value)
}

Boolean isContainLogsByValue(String value) {
getPbsLogsByValue(value) != null
}

private String getPbsLogsByValue(String value) {
pbsContainer.logs.split("\n").find { it.contains(value) }
}

<T> T getValueFromContainer(String path, Class<T> clazz) {
pbsContainer.copyFileFromContainer(path, { inputStream ->
return decode(inputStream, clazz)
Expand Down
Loading
Loading