• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.adservices.service.measurement.reporting;
18 
19 import static com.android.adservices.service.measurement.util.Applications.ANDROID_APP_SCHEME;
20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_DATASTORE_FAILURE;
21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR;
22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT;
23 
24 import android.net.Uri;
25 
26 import androidx.annotation.Nullable;
27 
28 import com.android.adservices.LoggerFactory;
29 import com.android.adservices.data.measurement.DatastoreException;
30 import com.android.adservices.data.measurement.IMeasurementDao;
31 import com.android.adservices.errorlogging.ErrorLogUtil;
32 import com.android.adservices.service.Flags;
33 import com.android.adservices.service.common.WebAddresses;
34 import com.android.adservices.service.measurement.Source;
35 import com.android.adservices.service.measurement.Trigger;
36 import com.android.adservices.service.measurement.aggregation.AggregateDebugReportData;
37 import com.android.adservices.service.measurement.aggregation.AggregateDebugReportRecord;
38 import com.android.adservices.service.measurement.aggregation.AggregateDebugReporting;
39 import com.android.adservices.service.measurement.aggregation.AggregateHistogramContribution;
40 import com.android.adservices.service.measurement.aggregation.AggregatePayloadGenerator;
41 import com.android.adservices.service.measurement.aggregation.AggregateReport;
42 import com.android.adservices.service.measurement.util.BaseUriExtractor;
43 import com.android.adservices.service.measurement.util.UnsignedLong;
44 
45 import org.json.JSONException;
46 
47 import java.math.BigInteger;
48 import java.util.ArrayList;
49 import java.util.Collection;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.Objects;
53 import java.util.Optional;
54 import java.util.Set;
55 import java.util.UUID;
56 import java.util.stream.Collectors;
57 import java.util.stream.IntStream;
58 
59 /**
60  * Generates and schedules aggregate debug reports in the supported ad-tech side erroneous cases.
61  */
62 public class AggregateDebugReportApi {
63     public static final String AGGREGATE_DEBUG_REPORT_API = "attribution-reporting-debug";
64     private final Flags mFlags;
65 
AggregateDebugReportApi(Flags flags)66     public AggregateDebugReportApi(Flags flags) {
67         mFlags = flags;
68     }
69 
70     /**
71      * Schedule debug reports for all source registration errors related, i.e. "source-*" debug
72      * reports.
73      */
scheduleSourceRegistrationDebugReport( Source source, Set<DebugReportApi.Type> types, IMeasurementDao measurementDao)74     public void scheduleSourceRegistrationDebugReport(
75             Source source, Set<DebugReportApi.Type> types, IMeasurementDao measurementDao) {
76         if (!mFlags.getMeasurementEnableAggregateDebugReporting()
77                 || source.getAggregateDebugReportingString() == null) {
78             logSkippedReport(
79                     String.format(
80                             "aggregatable_debug_reporting on source disabled; available=%s,"
81                                     + " flag=%s",
82                             source.getAggregateDebugReportingString() != null,
83                             mFlags.getMeasurementEnableAggregateDebugReporting()),
84                     new ArrayList<>(types),
85                     /* isSourceRegistrationError= */ true,
86                     source,
87                     /* trigger= */ null);
88             return;
89         }
90 
91         try {
92             AggregateDebugReporting sourceAdr = source.getAggregateDebugReportingObject();
93             List<AggregateDebugReportData> debugDataList =
94                     Optional.ofNullable(sourceAdr)
95                             .map(AggregateDebugReporting::getAggregateDebugReportDataList)
96                             .orElse(null);
97 
98             if (debugDataList == null || debugDataList.isEmpty()) {
99                 logSkippedReport(
100                         "Null or empty source aggregatable debug report data list",
101                         new ArrayList<>(types),
102                         /* isSourceRegistrationError= */ true,
103                         source,
104                         /* trigger= */ null);
105                 return;
106             }
107 
108             List<AggregateHistogramContribution> contributions =
109                     types.stream()
110                             .map(
111                                     type ->
112                                             getFirstMatchingAggregateReportData(debugDataList, type)
113                                                     .orElse(null))
114                             .filter(Objects::nonNull)
115                             .map(
116                                     debugData ->
117                                             createContributions(debugData, sourceAdr.getKeyPiece()))
118                             .collect(Collectors.toList());
119 
120             if (contributions.isEmpty()) {
121                 // Source have opted-in but the debug data didn't match
122                 logSkippedReport(
123                         "Debug report type data not opted-in for ADR",
124                         new ArrayList<>(types),
125                         /* isSourceRegistrationError= */ true,
126                         source,
127                         /* trigger= */ null);
128                 measurementDao.insertAggregateReport(generateNullAggregateReport(source));
129                 return;
130             }
131 
132             int sumNewContributions = sumContributions(contributions);
133             if (sumNewContributions + source.getAggregateDebugReportContributions()
134                     > sourceAdr.getBudget()) {
135                 logSkippedReport(
136                         "Source budget exceeded",
137                         new ArrayList<>(types),
138                         /* isSourceRegistrationError= */ true,
139                         source,
140                         /* trigger= */ null);
141                 measurementDao.insertAggregateReport(generateNullAggregateReport(source));
142                 return;
143             }
144 
145             Optional<Uri> baseOrigin = extractBaseUri(source.getRegistrationOrigin());
146             Optional<Uri> basePublisher = extractBaseUri(source.getPublisher());
147 
148             if (baseOrigin.isEmpty() || basePublisher.isEmpty()) {
149                 logSkippedReport(
150                         "Invalid origin or top level site",
151                         new ArrayList<>(types),
152                         /* isSourceRegistrationError= */ true,
153                         source,
154                         /* trigger= */ null);
155                 return;
156             }
157 
158             if (!isWithinRateLimits(
159                     baseOrigin.get(),
160                     basePublisher.get(),
161                     source.getPublisherType(),
162                     measurementDao,
163                     (source.getEventTime() - mFlags.getMeasurementAdrBudgetWindowLengthMillis()),
164                     sumNewContributions)) {
165                 logSkippedReport(
166                         "Rate limit exceeded",
167                         new ArrayList<>(types),
168                         /* isSourceRegistrationError= */ true,
169                         source,
170                         /* trigger= */ null);
171                 measurementDao.insertAggregateReport(generateNullAggregateReport(source));
172                 return;
173             }
174 
175             LoggerFactory.getMeasurementLogger()
176                     .d(
177                             "AggregateDebugReportApi::scheduleSourceRegistrationDebugReport:"
178                                 + " Generating debug report. Types: %s. Source ID: %s, Source Event"
179                                 + " ID: %s, Enrollment ID: %s",
180                             types, source.getId(), source.getEventId(), source.getEnrollmentId());
181 
182             // If the source is persisted in the DB, only then the resultant ADR should have the
183             // source ID for FKey constraint and per source reports consideration. Also, update
184             // the contributions in the DB if the source registration was successful.
185             String sourceId = null;
186             if (types.contains(DebugReportApi.Type.SOURCE_SUCCESS)
187                     || types.contains(DebugReportApi.Type.SOURCE_NOISED)) {
188                 source.setAggregateDebugContributions(
189                         sumNewContributions + source.getAggregateDebugReportContributions());
190                 measurementDao.updateSourceAggregateDebugContributions(source);
191                 sourceId = source.getId();
192             }
193 
194             AggregateReport aggregateReport =
195                     createAggregateReport(source, sourceId, contributions);
196             measurementDao.insertAggregateReport(aggregateReport);
197 
198             measurementDao.insertAggregateDebugReportRecord(
199                     createAggregateDebugReportRecord(
200                             aggregateReport,
201                             sumNewContributions,
202                             source.getRegistrant(),
203                             basePublisher.get(),
204                             baseOrigin.get()));
205         } catch (JSONException e) {
206             // This isn't expected as at this point all data is valid.
207             ErrorLogUtil.e(
208                     e,
209                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR,
210                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT);
211         } catch (DatastoreException e) {
212             // This isn't expected as at this point all data is valid.
213             ErrorLogUtil.e(
214                     e,
215                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_DATASTORE_FAILURE,
216                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT);
217         }
218     }
219 
220     /**
221      * Schedule debug reports for all trigger attribution errors related, i.e. "trigger-*", debug
222      * reports, except {@link DebugReportApi.Type#TRIGGER_NO_MATCHING_SOURCE}.
223      */
scheduleTriggerAttributionErrorWithSourceDebugReport( Source source, Trigger trigger, List<DebugReportApi.Type> types, IMeasurementDao measurementDao)224     public void scheduleTriggerAttributionErrorWithSourceDebugReport(
225             Source source,
226             Trigger trigger,
227             List<DebugReportApi.Type> types,
228             IMeasurementDao measurementDao) {
229         if (!mFlags.getMeasurementEnableAggregateDebugReporting()) {
230             logSkippedReport(
231                     String.format(
232                             "ADR on source disabled; trigger aggregatable_debug_reporting"
233                                     + " available=%s, flag=%s",
234                             trigger.getAggregateDebugReportingString() != null,
235                             mFlags.getMeasurementEnableAggregateDebugReporting()),
236                     types,
237                     /* isSourceRegistrationError= */ false,
238                     source,
239                     trigger);
240             return;
241         }
242 
243         try {
244             AggregateDebugReporting triggerAdr = trigger.getAggregateDebugReportingObject();
245             List<AggregateDebugReportData> triggerDebugDataList =
246                     Optional.ofNullable(triggerAdr)
247                             .map(AggregateDebugReporting::getAggregateDebugReportDataList)
248                             .orElse(null);
249             if (triggerDebugDataList == null || triggerDebugDataList.isEmpty()) {
250                 logSkippedReport(
251                         "Null or empty trigger aggregatable debug report data list",
252                         types,
253                         /* isSourceRegistrationError= */ false,
254                         source,
255                         trigger);
256                 return;
257             }
258 
259             AggregateDebugReporting sourceAdr = source.getAggregateDebugReportingObject();
260             if (sourceAdr == null) {
261                 logSkippedReport(
262                         "Source side aggregatable debug reporting is not available",
263                         types,
264                         /* isSourceRegistrationError= */ false,
265                         source,
266                         trigger);
267                 measurementDao.insertAggregateReport(generateNullAggregateReport(source, trigger));
268                 return;
269             }
270 
271             List<AggregateHistogramContribution> contributions =
272                     types.stream()
273                             .map(
274                                     type ->
275                                             getFirstMatchingAggregateReportData(
276                                                             triggerDebugDataList, type)
277                                                     .orElse(null))
278                             .filter(Objects::nonNull)
279                             .map(
280                                     debugData ->
281                                             createContributions(
282                                                     debugData,
283                                                     sourceAdr
284                                                             .getKeyPiece()
285                                                             .or(triggerAdr.getKeyPiece())))
286                             .collect(Collectors.toList());
287 
288             if (contributions.isEmpty()) {
289                 // Both Source and trigger have opted-in but the debug data didn't match
290                 logSkippedReport(
291                         "Debug report type data not opted-in for ADR",
292                         types,
293                         /* isSourceRegistrationError= */ false,
294                         source,
295                         trigger);
296                 measurementDao.insertAggregateReport(generateNullAggregateReport(source, trigger));
297                 return;
298             }
299 
300             int sumNewContributions = sumContributions(contributions);
301             if (sumNewContributions + source.getAggregateDebugReportContributions()
302                     > sourceAdr.getBudget()) {
303                 logSkippedReport(
304                         "Source budget exceeded",
305                         types,
306                         /* isSourceRegistrationError= */ false,
307                         source,
308                         trigger);
309                 measurementDao.insertAggregateReport(generateNullAggregateReport(source, trigger));
310                 return;
311             }
312 
313             if (measurementDao.countNumAggregateReportsPerSource(
314                             source.getId(), AGGREGATE_DEBUG_REPORT_API)
315                     >= mFlags.getMeasurementMaxAdrCountPerSource()) {
316                 logSkippedReport(
317                         "Exceeded max number of reports per source",
318                         types,
319                         /* isSourceRegistrationError= */ false,
320                         source,
321                         trigger);
322                 measurementDao.insertAggregateReport(generateNullAggregateReport(source, trigger));
323                 return;
324             }
325 
326             Optional<Uri> baseOrigin = extractBaseUri(trigger.getRegistrationOrigin());
327             Uri baseTopLevelSite = trigger.getAttributionDestinationBaseUri();
328 
329             if (baseOrigin.isEmpty() || baseTopLevelSite == null) {
330                 logSkippedReport(
331                         "Invalid origin or top level site",
332                         types,
333                         /* isSourceRegistrationError= */ false,
334                         source,
335                         trigger);
336                 return;
337             }
338 
339             if (!isWithinRateLimits(
340                     baseOrigin.get(),
341                     baseTopLevelSite,
342                     trigger.getDestinationType(),
343                     measurementDao,
344                     (trigger.getTriggerTime() - mFlags.getMeasurementAdrBudgetWindowLengthMillis()),
345                     sumNewContributions)) {
346                 logSkippedReport(
347                         "Rate limit exceeded",
348                         types,
349                         /* isSourceRegistrationError= */ false,
350                         source,
351                         trigger);
352                 measurementDao.insertAggregateReport(generateNullAggregateReport(source, trigger));
353                 return;
354             }
355 
356             LoggerFactory.getMeasurementLogger()
357                     .d(
358                             "AggregateDebugReportApi::scheduleTriggerAttributionErrorWithSource"
359                                 + "DebugReport: Generating debug report. Types: %s. Trigger ID: %s,"
360                                 + " Source ID: %s, Source Event ID: %s, Enrollment ID: %s",
361                             types,
362                             trigger.getId(),
363                             source.getId(),
364                             source.getEventId(),
365                             source.getEnrollmentId());
366             AggregateReport aggregateReport = createAggregateReport(source, trigger, contributions);
367             measurementDao.insertAggregateReport(aggregateReport);
368             measurementDao.insertAggregateDebugReportRecord(
369                     createAggregateDebugReportRecord(
370                             aggregateReport,
371                             sumNewContributions,
372                             trigger.getRegistrant(),
373                             baseTopLevelSite,
374                             baseOrigin.get()));
375 
376             source.setAggregateDebugContributions(
377                     sumNewContributions + source.getAggregateDebugReportContributions());
378             measurementDao.updateSourceAggregateDebugContributions(source);
379         } catch (JSONException e) {
380             // This isn't expected as at this point all data is valid.
381             ErrorLogUtil.e(
382                     e,
383                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR,
384                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT);
385         } catch (DatastoreException e) {
386             // This isn't expected as at this point all data is valid.
387             ErrorLogUtil.e(
388                     e,
389                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_DATASTORE_FAILURE,
390                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT);
391         }
392     }
393 
394     /**
395      * Create aggregate debug report for {@link DebugReportApi.Type#TRIGGER_NO_MATCHING_SOURCE}
396      * case. It's different from {@link #scheduleTriggerAttributionErrorWithSourceDebugReport}
397      * because source isn't available hence contribution budget doesn't apply.
398      */
scheduleTriggerNoMatchingSourceDebugReport( Trigger trigger, IMeasurementDao measurementDao)399     public void scheduleTriggerNoMatchingSourceDebugReport(
400             Trigger trigger, IMeasurementDao measurementDao) {
401         if (!mFlags.getMeasurementEnableAggregateDebugReporting()
402                 || trigger.getAggregateDebugReportingString() == null) {
403             logSkippedNoMatchingSourceReport(
404                     String.format(
405                             "ADR on source disabled; trigger aggregatable_debug_reporting"
406                                     + " available=%s, flag=%s",
407                             trigger.getAggregateDebugReportingString() != null,
408                             mFlags.getMeasurementEnableAggregateDebugReporting()),
409                     trigger);
410             return;
411         }
412 
413         try {
414             AggregateDebugReporting triggerAdr = trigger.getAggregateDebugReportingObject();
415             if (triggerAdr == null
416                     || triggerAdr.getAggregateDebugReportDataList() == null
417                     || triggerAdr.getAggregateDebugReportDataList().isEmpty()) {
418                 logSkippedNoMatchingSourceReport(
419                         "Trigger aggregatable debug reporting object is null or"
420                                 + " data list is null / empty",
421                         trigger);
422                 return;
423             }
424             DebugReportApi.Type type = DebugReportApi.Type.TRIGGER_NO_MATCHING_SOURCE;
425             Optional<AggregateDebugReportData> firstMatchingAggregateReportData =
426                     getFirstMatchingAggregateReportData(
427                             triggerAdr.getAggregateDebugReportDataList(), type);
428             if (firstMatchingAggregateReportData.isEmpty()) {
429                 logSkippedNoMatchingSourceReport("Skipping aggregate report", trigger);
430                 measurementDao.insertAggregateReport(generateNullAggregateReport(trigger));
431                 return;
432             }
433 
434             AggregateDebugReportData errorDebugReportingData =
435                     firstMatchingAggregateReportData.get();
436 
437             Optional<Uri> baseOrigin = extractBaseUri(trigger.getRegistrationOrigin());
438             Uri baseTopLevelSite = trigger.getAttributionDestinationBaseUri();
439             if (baseOrigin.isEmpty() || baseTopLevelSite == null) {
440                 logSkippedNoMatchingSourceReport("Invalid origin or top level site", trigger);
441                 return;
442             }
443 
444             if (!isWithinRateLimits(
445                     baseOrigin.get(),
446                     baseTopLevelSite,
447                     trigger.getDestinationType(),
448                     measurementDao,
449                     (trigger.getTriggerTime() - mFlags.getMeasurementAdrBudgetWindowLengthMillis()),
450                     errorDebugReportingData.getValue())) {
451                 logSkippedNoMatchingSourceReport("Rate limit exceeded", trigger);
452                 measurementDao.insertAggregateReport(generateNullAggregateReport(trigger));
453                 return;
454             }
455 
456             AggregateHistogramContribution contributions =
457                     createContributions(
458                             errorDebugReportingData,
459                             trigger.getAggregateDebugReportingObject().getKeyPiece());
460 
461             LoggerFactory.getMeasurementLogger()
462                     .d(
463                             "AggregateDebugReportApi: Generating debug report %s. "
464                                     + "Trigger ID: %s, Enrollment ID: %s",
465                             type, trigger.getId(), trigger.getEnrollmentId());
466             AggregateReport aggregateReport = createAggregateReport(trigger, contributions);
467             measurementDao.insertAggregateReport(aggregateReport);
468             measurementDao.insertAggregateDebugReportRecord(
469                     createAggregateDebugReportRecord(
470                             aggregateReport,
471                             contributions.getValue(),
472                             trigger.getRegistrant(),
473                             baseTopLevelSite,
474                             baseOrigin.get()));
475         } catch (JSONException e) {
476             // This isn't expected as at this point all data is valid.
477             ErrorLogUtil.e(
478                     e,
479                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_REPORTING_PARSING_ERROR,
480                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT);
481         } catch (DatastoreException e) {
482             // This isn't expected as at this point all data is valid.
483             ErrorLogUtil.e(
484                     e,
485                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__MEASUREMENT_DATASTORE_FAILURE,
486                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT);
487         }
488     }
489 
logSkippedReport( String validationErrorMsg, List<DebugReportApi.Type> types, Boolean isSourceRegistrationError, Source source, @Nullable Trigger trigger)490     private void logSkippedReport(
491             String validationErrorMsg,
492             List<DebugReportApi.Type> types,
493             Boolean isSourceRegistrationError,
494             Source source,
495             @Nullable Trigger trigger) {
496         String maybeGetTriggerId =
497                 trigger != null ? String.format(", Trigger ID: %s", trigger.getId()) : "";
498 
499         LoggerFactory.getMeasurementLogger()
500                 .d(
501                         "AggregateDebugReportApi::%s (REPORT SKIPPED): %s. Types: %s. Enrollment"
502                                 + " ID: %s, Source ID: %s, Source Event ID: %s%s",
503                         isSourceRegistrationError
504                                 ? "scheduleSourceRegistrationDebugReport"
505                                 : "scheduleTriggerAttributionErrorWithSourceDebugReport",
506                         validationErrorMsg,
507                         types,
508                         source.getEnrollmentId(),
509                         source.getId(),
510                         source.getEventId(),
511                         maybeGetTriggerId);
512     }
513 
logSkippedNoMatchingSourceReport(String validationErrorMsg, Trigger trigger)514     private void logSkippedNoMatchingSourceReport(String validationErrorMsg, Trigger trigger) {
515         LoggerFactory.getMeasurementLogger()
516                 .d(
517                         "AggregateDebugReportApi::scheduleTriggerNoMatchingSourceDebugReport"
518                             + " (REPORT SKIPPED): %s. Type: %s. Enrollment ID: %s, Trigger ID: %s",
519                         validationErrorMsg,
520                         DebugReportApi.Type.TRIGGER_NO_MATCHING_SOURCE,
521                         trigger.getEnrollmentId(),
522                         trigger.getId());
523     }
524 
getTriggerOrDefaultCoordinatorOrigin( AggregateDebugReporting aggregateDebugReportingObject)525     private Uri getTriggerOrDefaultCoordinatorOrigin(
526             AggregateDebugReporting aggregateDebugReportingObject) {
527         return Optional.ofNullable(aggregateDebugReportingObject.getAggregationCoordinatorOrigin())
528                 .orElse(Uri.parse(mFlags.getMeasurementDefaultAggregationCoordinatorOrigin()));
529     }
530 
createAggregateReport( Trigger trigger, AggregateHistogramContribution contributions)531     private AggregateReport createAggregateReport(
532             Trigger trigger, AggregateHistogramContribution contributions) throws JSONException {
533         Uri coordinatorOrigin =
534                 getTriggerOrDefaultCoordinatorOrigin(trigger.getAggregateDebugReportingObject());
535         return new AggregateReport.Builder()
536                 .setId(UUID.randomUUID().toString())
537                 .setAttributionDestination(trigger.getAttributionDestinationBaseUri())
538                 .setPublisher(trigger.getAttributionDestination())
539                 .setScheduledReportTime(trigger.getTriggerTime())
540                 .setEnrollmentId(trigger.getEnrollmentId())
541                 .setDebugCleartextPayload(
542                         AggregateReport.generateDebugPayload(
543                                 getPaddedContributions(Collections.singletonList(contributions))))
544                 // We don't want to deliver regular aggregate reports
545                 .setStatus(AggregateReport.Status.MARKED_TO_DELETE)
546                 .setDebugReportStatus(AggregateReport.DebugReportStatus.PENDING)
547                 .setApiVersion(AggregatePayloadGenerator.getApiVersion(mFlags))
548                 .setSourceId(null)
549                 .setTriggerId(trigger.getId())
550                 .setRegistrationOrigin(trigger.getRegistrationOrigin())
551                 .setApi(AGGREGATE_DEBUG_REPORT_API)
552                 .setAggregationCoordinatorOrigin(coordinatorOrigin)
553                 .build();
554     }
555 
createAggregateReport( Source source, String sourceId, List<AggregateHistogramContribution> contributions)556     private AggregateReport createAggregateReport(
557             Source source, String sourceId, List<AggregateHistogramContribution> contributions)
558             throws JSONException {
559         return new AggregateReport.Builder()
560                 .setId(UUID.randomUUID().toString())
561                 .setPublisher(source.getPublisher())
562                 // Source already has base destination URIs
563                 .setAttributionDestination(getSourceDestinationToReport(source))
564                 .setScheduledReportTime(source.getEventTime())
565                 .setEnrollmentId(source.getEnrollmentId())
566                 .setDebugCleartextPayload(
567                         AggregateReport.generateDebugPayload(getPaddedContributions(contributions)))
568                 // We don't want to deliver regular aggregate reports for ADRs
569                 .setStatus(AggregateReport.Status.MARKED_TO_DELETE)
570                 .setDebugReportStatus(AggregateReport.DebugReportStatus.PENDING)
571                 .setApiVersion(AggregatePayloadGenerator.getApiVersion(mFlags))
572                 .setSourceId(sourceId)
573                 .setTriggerId(null)
574                 .setRegistrationOrigin(source.getRegistrationOrigin())
575                 .setApi(AGGREGATE_DEBUG_REPORT_API)
576                 .setAggregationCoordinatorOrigin(
577                         Uri.parse(mFlags.getMeasurementDefaultAggregationCoordinatorOrigin()))
578                 .build();
579     }
580 
createAggregateReport( Source source, Trigger trigger, List<AggregateHistogramContribution> contributions)581     private AggregateReport createAggregateReport(
582             Source source, Trigger trigger, List<AggregateHistogramContribution> contributions)
583             throws JSONException {
584         Uri coordinatorOrigin =
585                 getTriggerOrDefaultCoordinatorOrigin(trigger.getAggregateDebugReportingObject());
586         return new AggregateReport.Builder()
587                 .setId(UUID.randomUUID().toString())
588                 .setPublisher(source.getPublisher())
589                 .setAttributionDestination(trigger.getAttributionDestinationBaseUri())
590                 .setScheduledReportTime(trigger.getTriggerTime())
591                 .setEnrollmentId(source.getEnrollmentId())
592                 .setDebugCleartextPayload(
593                         AggregateReport.generateDebugPayload(getPaddedContributions(contributions)))
594                 // We don't want to deliver regular aggregate reports
595                 .setStatus(AggregateReport.Status.MARKED_TO_DELETE)
596                 .setDebugReportStatus(AggregateReport.DebugReportStatus.PENDING)
597                 .setApiVersion(AggregatePayloadGenerator.getApiVersion(mFlags))
598                 // As source/trigger registration might have failed
599                 .setSourceId(source.getId())
600                 .setTriggerId(trigger.getId())
601                 .setRegistrationOrigin(trigger.getRegistrationOrigin())
602                 .setApi(AGGREGATE_DEBUG_REPORT_API)
603                 .setAggregationCoordinatorOrigin(coordinatorOrigin)
604                 .build();
605     }
606 
isWithinRateLimits( Uri origin, Uri topLevelSite, int topLevelSiteType, IMeasurementDao measurementDao, long windowStartTime, int newContributions)607     private boolean isWithinRateLimits(
608             Uri origin,
609             Uri topLevelSite,
610             int topLevelSiteType,
611             IMeasurementDao measurementDao,
612             long windowStartTime,
613             int newContributions)
614             throws DatastoreException {
615         // Per origin per topLevelSite limits
616         if ((measurementDao.sumAggregateDebugReportBudgetXOriginXPublisherXWindow(
617                                 topLevelSite, topLevelSiteType, origin, windowStartTime)
618                         + newContributions)
619                 > mFlags.getMeasurementAdrBudgetOriginXPublisherXWindow()) {
620             return false;
621         }
622 
623         // Per topLevelSite limits
624         if ((measurementDao.sumAggregateDebugReportBudgetXPublisherXWindow(
625                                 topLevelSite, topLevelSiteType, windowStartTime)
626                         + newContributions)
627                 > mFlags.getMeasurementAdrBudgetPublisherXWindow()) {
628             return false;
629         }
630 
631         return true;
632     }
633 
getFirstMatchingAggregateReportData( Collection<AggregateDebugReportData> aggregateDebugReportDataList, DebugReportApi.Type reportType)634     private Optional<AggregateDebugReportData> getFirstMatchingAggregateReportData(
635             Collection<AggregateDebugReportData> aggregateDebugReportDataList,
636             DebugReportApi.Type reportType) {
637         if (aggregateDebugReportDataList == null) {
638             LoggerFactory.getMeasurementLogger()
639                     .d(
640                             "AggregateDebugReportApi: No matching debug data to generate"
641                                     + " aggregatable report. Type: %s",
642                             reportType);
643             return Optional.empty();
644         }
645         Optional<AggregateDebugReportData> unspecifiedDebugData = Optional.empty();
646         for (AggregateDebugReportData data : aggregateDebugReportDataList) {
647             for (String type : data.getReportType()) {
648                 if (type.equals(reportType.getValue())) {
649                     return Optional.of(data);
650                 }
651                 if (type.equals(DebugReportApi.Type.UNSPECIFIED.getValue())) {
652                     unspecifiedDebugData = Optional.of(data);
653                 }
654             }
655         }
656 
657         if (unspecifiedDebugData.isEmpty()) {
658             LoggerFactory.getMeasurementLogger()
659                     .d(
660                             "AggregateDebugReportApi: No matching debug data to generate"
661                                     + " aggregatable report. Type: %s",
662                             reportType);
663         }
664         return unspecifiedDebugData;
665     }
666 
getSourceDestinationToReport(Source source)667     private Uri getSourceDestinationToReport(Source source) {
668         return (source.getAppDestinations() == null || source.getAppDestinations().isEmpty())
669                 ? Collections.min(source.getWebDestinations())
670                 : Collections.min(source.getAppDestinations());
671     }
672 
createContributions( AggregateDebugReportData errorDebugReportingData, BigInteger keyPiece)673     private AggregateHistogramContribution createContributions(
674             AggregateDebugReportData errorDebugReportingData, BigInteger keyPiece) {
675         AggregateHistogramContribution.Builder aggregateHistogramContributionBuilder =
676                 new AggregateHistogramContribution.Builder()
677                         .setKey(keyPiece.or(errorDebugReportingData.getKeyPiece()))
678                         .setValue(errorDebugReportingData.getValue());
679         if (mFlags.getMeasurementEnableFlexibleContributionFiltering()) {
680             aggregateHistogramContributionBuilder.setId(UnsignedLong.ZERO);
681         }
682         return aggregateHistogramContributionBuilder.build();
683     }
684 
sumContributions(List<AggregateHistogramContribution> contributions)685     private static int sumContributions(List<AggregateHistogramContribution> contributions) {
686         return contributions.stream().mapToInt(AggregateHistogramContribution::getValue).sum();
687     }
688 
createAggregateDebugReportRecord( AggregateReport aggregateReport, int contributionValue, Uri registrantApp, Uri topLevelSite, Uri origin)689     private static AggregateDebugReportRecord createAggregateDebugReportRecord(
690             AggregateReport aggregateReport,
691             int contributionValue,
692             Uri registrantApp,
693             Uri topLevelSite,
694             Uri origin) {
695         return new AggregateDebugReportRecord.Builder(
696                         aggregateReport.getScheduledReportTime(),
697                         topLevelSite,
698                         registrantApp,
699                         origin,
700                         contributionValue)
701                 .setSourceId(aggregateReport.getSourceId())
702                 .setTriggerId(aggregateReport.getTriggerId())
703                 .build();
704     }
705 
generateNullAggregateReport(Source source, Trigger trigger)706     private AggregateReport generateNullAggregateReport(Source source, Trigger trigger)
707             throws JSONException {
708         return generateBaseNullReportBuilder()
709                 .setRegistrationOrigin(trigger.getRegistrationOrigin())
710                 .setAttributionDestination(trigger.getAttributionDestinationBaseUri())
711                 .setScheduledReportTime(trigger.getTriggerTime())
712                 .setTriggerId(trigger.getId())
713                 .setAggregationCoordinatorOrigin(
714                         getTriggerOrDefaultCoordinatorOrigin(
715                                 trigger.getAggregateDebugReportingObject()))
716                 .setPublisher(source.getPublisher())
717                 .build();
718     }
719 
generateNullAggregateReport(Source source)720     private AggregateReport generateNullAggregateReport(Source source) throws JSONException {
721         return generateBaseNullReportBuilder()
722                 .setPublisher(source.getPublisher())
723                 .setRegistrationOrigin(source.getRegistrationOrigin())
724                 // Source already has base destination URIs
725                 .setAttributionDestination(getSourceDestinationToReport(source))
726                 .setScheduledReportTime(source.getEventTime())
727                 // We don't want null report to be counted as this source driven ADR
728                 .setSourceId(null)
729                 .setAggregationCoordinatorOrigin(
730                         Uri.parse(mFlags.getMeasurementDefaultAggregationCoordinatorOrigin()))
731                 .build();
732     }
733 
generateNullAggregateReport(Trigger trigger)734     private AggregateReport generateNullAggregateReport(Trigger trigger) throws JSONException {
735         return generateBaseNullReportBuilder()
736                 .setRegistrationOrigin(trigger.getRegistrationOrigin())
737                 .setAttributionDestination(trigger.getAttributionDestinationBaseUri())
738                 .setScheduledReportTime(trigger.getTriggerTime())
739                 .setTriggerId(trigger.getId())
740                 .setAggregationCoordinatorOrigin(
741                         getTriggerOrDefaultCoordinatorOrigin(
742                                 trigger.getAggregateDebugReportingObject()))
743                 .build();
744     }
745 
generateBaseNullReportBuilder()746     private AggregateReport.Builder generateBaseNullReportBuilder() throws JSONException {
747         String debugPayload =
748                 AggregateReport.generateDebugPayload(
749                         getPaddedContributions(
750                                 Collections.singletonList(createPaddingContribution())));
751         return new AggregateReport.Builder()
752                 .setId(UUID.randomUUID().toString())
753                 .setApiVersion(AggregatePayloadGenerator.getApiVersion(mFlags))
754                 // exclude by default
755                 .setSourceRegistrationTime(null)
756                 .setDebugCleartextPayload(debugPayload)
757                 .setIsFakeReport(true)
758                 .setTriggerContextId(null)
759                 .setApi(AGGREGATE_DEBUG_REPORT_API)
760                 .setAggregationCoordinatorOrigin(
761                         Uri.parse(mFlags.getMeasurementDefaultAggregationCoordinatorOrigin()))
762                 .setDebugReportStatus(AggregateReport.DebugReportStatus.PENDING)
763                 .setStatus(AggregateReport.Status.MARKED_TO_DELETE);
764     }
765 
getPaddedContributions( List<AggregateHistogramContribution> contributions)766     private List<AggregateHistogramContribution> getPaddedContributions(
767             List<AggregateHistogramContribution> contributions) {
768         List<AggregateHistogramContribution> paddedContributions = new ArrayList<>(contributions);
769         IntStream.range(
770                         contributions.size(),
771                         mFlags.getMeasurementMaxAggregateKeysPerSourceRegistration())
772                 .forEach(i -> paddedContributions.add(createPaddingContribution()));
773         return paddedContributions;
774     }
775 
createPaddingContribution()776     private AggregateHistogramContribution createPaddingContribution() {
777         AggregateHistogramContribution.Builder aggregateHistogramContributionBuilder =
778                 new AggregateHistogramContribution.Builder();
779         if (mFlags.getMeasurementEnableFlexibleContributionFiltering()) {
780             aggregateHistogramContributionBuilder.setPaddingContributionWithFilteringId();
781         } else {
782             aggregateHistogramContributionBuilder.setPaddingContribution();
783         }
784         return aggregateHistogramContributionBuilder.build();
785     }
786 
extractBaseUri(Uri uri)787     private static Optional<Uri> extractBaseUri(Uri uri) {
788         if (uri.getScheme().equals(ANDROID_APP_SCHEME)) {
789             return Optional.of(BaseUriExtractor.getBaseUri(uri));
790         }
791         return WebAddresses.topPrivateDomainAndScheme(uri);
792     }
793 }
794