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