1 /* 2 * Copyright (C) 2022 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 android.annotation.NonNull; 20 import android.content.Context; 21 import android.net.Uri; 22 import android.util.Pair; 23 24 import androidx.annotation.Nullable; 25 26 import com.android.adservices.LoggerFactory; 27 import com.android.adservices.data.measurement.DatastoreException; 28 import com.android.adservices.data.measurement.IMeasurementDao; 29 import com.android.adservices.service.Flags; 30 import com.android.adservices.service.common.WebAddresses; 31 import com.android.adservices.service.measurement.EventSurfaceType; 32 import com.android.adservices.service.measurement.Source; 33 import com.android.adservices.service.measurement.Trigger; 34 import com.android.adservices.service.measurement.noising.SourceNoiseHandler; 35 import com.android.adservices.service.measurement.util.UnsignedLong; 36 import com.android.internal.annotations.VisibleForTesting; 37 38 import org.json.JSONException; 39 import org.json.JSONObject; 40 41 import java.util.ArrayList; 42 import java.util.HashMap; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Objects; 46 import java.util.Optional; 47 import java.util.UUID; 48 import java.util.concurrent.TimeUnit; 49 50 /** Class used to send debug reports to Ad-Tech {@link DebugReport} */ 51 public class DebugReportApi { 52 53 /** Define different verbose debug report types. */ 54 public enum Type { 55 UNSPECIFIED("unspecified"), 56 SOURCE_DESTINATION_LIMIT("source-destination-limit"), 57 SOURCE_DESTINATION_RATE_LIMIT("source-destination-rate-limit"), 58 SOURCE_DESTINATION_PER_DAY_RATE_LIMIT("source-destination-per-day-rate-limit"), 59 SOURCE_NOISED("source-noised"), 60 SOURCE_STORAGE_LIMIT("source-storage-limit"), 61 SOURCE_SUCCESS("source-success"), 62 SOURCE_UNKNOWN_ERROR("source-unknown-error"), 63 SOURCE_FLEXIBLE_EVENT_REPORT_VALUE_ERROR("source-flexible-event-report-value-error"), 64 SOURCE_MAX_EVENT_STATES_LIMIT("source-max-event-states-limit"), 65 SOURCE_SCOPES_CHANNEL_CAPACITY_LIMIT("source-scopes-channel-capacity-limit"), 66 SOURCE_CHANNEL_CAPACITY_LIMIT("source-channel-capacity-limit"), 67 SOURCE_ATTRIBUTION_SCOPE_INFO_GAIN_LIMIT("source-attribution-scope-info-gain-limit"), 68 SOURCE_DESTINATION_GLOBAL_RATE_LIMIT("source-destination-global-rate-limit"), 69 SOURCE_DESTINATION_LIMIT_REPLACED("source-destination-limit-replaced"), 70 SOURCE_REPORTING_ORIGIN_LIMIT("source-reporting-origin-limit"), 71 SOURCE_REPORTING_ORIGIN_PER_SITE_LIMIT("source-reporting-origin-per-site-limit"), 72 SOURCE_TRIGGER_STATE_CARDINALITY_LIMIT("source-trigger-state-cardinality-limit"), 73 74 TRIGGER_AGGREGATE_DEDUPLICATED("trigger-aggregate-deduplicated"), 75 TRIGGER_AGGREGATE_INSUFFICIENT_BUDGET("trigger-aggregate-insufficient-budget"), 76 TRIGGER_AGGREGATE_NO_CONTRIBUTIONS("trigger-aggregate-no-contributions"), 77 TRIGGER_AGGREGATE_REPORT_WINDOW_PASSED("trigger-aggregate-report-window-passed"), 78 TRIGGER_ATTRIBUTIONS_PER_SOURCE_DESTINATION_LIMIT( 79 "trigger-attributions-per-source-destination-limit"), 80 TRIGGER_EVENT_ATTRIBUTIONS_PER_SOURCE_DESTINATION_LIMIT( 81 "trigger-event-attributions-per-source-destination-limit"), 82 TRIGGER_AGGREGATE_ATTRIBUTIONS_PER_SOURCE_DESTINATION_LIMIT( 83 "trigger-aggregate-attributions-per-source-destination-limit"), 84 TRIGGER_EVENT_DEDUPLICATED("trigger-event-deduplicated"), 85 TRIGGER_EVENT_EXCESSIVE_REPORTS("trigger-event-excessive-reports"), 86 TRIGGER_EVENT_LOW_PRIORITY("trigger-event-low-priority"), 87 TRIGGER_EVENT_NO_MATCHING_CONFIGURATIONS("trigger-event-no-matching-configurations"), 88 TRIGGER_EVENT_NOISE("trigger-event-noise"), 89 TRIGGER_EVENT_REPORT_WINDOW_PASSED("trigger-event-report-window-passed"), 90 TRIGGER_NO_MATCHING_FILTER_DATA("trigger-no-matching-filter-data"), 91 TRIGGER_NO_MATCHING_SOURCE("trigger-no-matching-source"), 92 TRIGGER_REPORTING_ORIGIN_LIMIT("trigger-reporting-origin-limit"), 93 TRIGGER_EVENT_STORAGE_LIMIT("trigger-event-storage-limit"), 94 TRIGGER_UNKNOWN_ERROR("trigger-unknown-error"), 95 TRIGGER_AGGREGATE_STORAGE_LIMIT("trigger-aggregate-storage-limit"), 96 TRIGGER_AGGREGATE_EXCESSIVE_REPORTS("trigger-aggregate-excessive-reports"), 97 TRIGGER_EVENT_REPORT_WINDOW_NOT_STARTED("trigger-event-report-window-not-started"), 98 TRIGGER_EVENT_NO_MATCHING_TRIGGER_DATA("trigger-event-no-matching-trigger-data"), 99 HEADER_PARSING_ERROR("header-parsing-error"), 100 TRIGGER_AGGREGATE_INSUFFICIENT_NAMED_BUDGET("trigger-aggregate-insufficient-named-budget"); 101 102 private final String mValue; 103 Type(String value)104 Type(String value) { 105 mValue = value; 106 } 107 getValue()108 public String getValue() { 109 return mValue; 110 } 111 112 /** get enum type from string value */ findByValue(String value)113 public static Optional<Type> findByValue(String value) { 114 for (Type type : values()) { 115 if (type.getValue().equalsIgnoreCase(value)) { 116 return Optional.of(type); 117 } 118 } 119 return Optional.empty(); 120 } 121 } 122 123 /** Defines different verbose debug report body parameters. */ 124 @VisibleForTesting 125 public interface Body { 126 String ATTRIBUTION_DESTINATION = "attribution_destination"; 127 String LIMIT = "limit"; 128 String NAME = "name"; 129 String RANDOMIZED_TRIGGER_RATE = "randomized_trigger_rate"; 130 String SCHEDULED_REPORT_TIME = "scheduled_report_time"; 131 String SOURCE_DEBUG_KEY = "source_debug_key"; 132 String SOURCE_EVENT_ID = "source_event_id"; 133 String SOURCE_SITE = "source_site"; 134 String SOURCE_TYPE = "source_type"; 135 String TRIGGER_DATA = "trigger_data"; 136 String TRIGGER_DEBUG_KEY = "trigger_debug_key"; 137 String SOURCE_DESTINATION_LIMIT = "source_destination_limit"; 138 String CONTEXT_SITE = "context_site"; 139 String HEADER = "header"; 140 String VALUE = "value"; 141 String ERROR = "error"; 142 } 143 144 private enum PermissionState { 145 GRANTED, 146 DENIED, 147 NONE 148 } 149 150 private final Context mContext; 151 private final Flags mFlags; 152 private final EventReportWindowCalcDelegate mEventReportWindowCalcDelegate; 153 private final SourceNoiseHandler mSourceNoiseHandler; 154 DebugReportApi(Context context, Flags flags)155 public DebugReportApi(Context context, Flags flags) { 156 this( 157 context, 158 flags, 159 new EventReportWindowCalcDelegate(flags), 160 new SourceNoiseHandler(flags)); 161 } 162 163 @VisibleForTesting DebugReportApi( Context context, Flags flags, EventReportWindowCalcDelegate eventReportWindowCalcDelegate, SourceNoiseHandler sourceNoiseHandler)164 public DebugReportApi( 165 Context context, 166 Flags flags, 167 EventReportWindowCalcDelegate eventReportWindowCalcDelegate, 168 SourceNoiseHandler sourceNoiseHandler) { 169 mContext = context; 170 mFlags = flags; 171 mEventReportWindowCalcDelegate = eventReportWindowCalcDelegate; 172 mSourceNoiseHandler = sourceNoiseHandler; 173 } 174 175 /** Schedules the Source Destination limit Debug Report */ scheduleSourceDestinationLimitDebugReport( Source source, String limit, IMeasurementDao dao)176 public void scheduleSourceDestinationLimitDebugReport( 177 Source source, String limit, IMeasurementDao dao) { 178 scheduleSourceDestinationLimitDebugReport( 179 source, limit, Type.SOURCE_DESTINATION_LIMIT, dao); 180 } 181 182 /** Schedules the Source Destination rate-limit Debug Report */ scheduleSourceDestinationPerMinuteRateLimitDebugReport( Source source, String limit, IMeasurementDao dao)183 public void scheduleSourceDestinationPerMinuteRateLimitDebugReport( 184 Source source, String limit, IMeasurementDao dao) { 185 scheduleSourceDestinationLimitDebugReport( 186 source, limit, Type.SOURCE_DESTINATION_RATE_LIMIT, dao); 187 } 188 189 /** Schedules the Source Destination per day rate-limit Debug Report */ scheduleSourceDestinationPerDayRateLimitDebugReport( Source source, String limit, IMeasurementDao dao)190 public void scheduleSourceDestinationPerDayRateLimitDebugReport( 191 Source source, String limit, IMeasurementDao dao) { 192 scheduleSourceDestinationLimitDebugReport( 193 source, limit, Type.SOURCE_DESTINATION_PER_DAY_RATE_LIMIT, dao); 194 } 195 196 /** Schedules Source Attribution Scope Debug Report */ scheduleAttributionScopeDebugReport( Source source, Source.AttributionScopeValidationResult result, IMeasurementDao dao)197 public DebugReportApi.Type scheduleAttributionScopeDebugReport( 198 Source source, Source.AttributionScopeValidationResult result, IMeasurementDao dao) { 199 DebugReportApi.Type type = null; 200 Map<String, Object> additionalBodyParams = new HashMap<>(); 201 switch (result) { 202 case VALID -> { 203 // No-op. 204 } 205 case INVALID_MAX_EVENT_STATES_LIMIT -> { 206 type = Type.SOURCE_MAX_EVENT_STATES_LIMIT; 207 additionalBodyParams.put(Body.LIMIT, String.valueOf(source.getMaxEventStates())); 208 } 209 case INVALID_INFORMATION_GAIN_LIMIT -> { 210 type = Type.SOURCE_SCOPES_CHANNEL_CAPACITY_LIMIT; 211 additionalBodyParams.put( 212 Body.LIMIT, source.getAttributionScopeInfoGainThreshold(mFlags)); 213 } 214 } 215 if (type == null) { 216 return null; 217 } 218 scheduleSourceReport(source, type, additionalBodyParams, dao); 219 return type; 220 } 221 222 /** Determines if scheduling the Trigger Report is allowed */ isTriggerReportAllowed( Trigger trigger, DebugReportApi.Type type, @Nullable Source source)223 private boolean isTriggerReportAllowed( 224 Trigger trigger, DebugReportApi.Type type, @Nullable Source source) { 225 Objects.requireNonNull(trigger, "trigger cannot be null"); 226 Objects.requireNonNull(type, "type cannot be null"); 227 228 if (isTriggerDebugFlagDisabled(type, trigger, source)) { 229 return false; 230 } 231 if (isAdTechNotOptIn(trigger.isDebugReporting(), type, source, trigger)) { 232 return false; 233 } 234 235 if (!isSourceAndTriggerPermissionsGranted(source, trigger)) { 236 LoggerFactory.getMeasurementLogger() 237 .d( 238 "DebugReportApi: Skipping trigger debug report %s. Trigger ID: %s," 239 + " Enrollment ID: %s%s", 240 type, 241 trigger.getId(), 242 trigger.getEnrollmentId(), 243 maybeGetSourceInfo(source)); 244 return false; 245 } 246 247 return true; 248 } 249 250 /** 251 * Schedules trigger-no-matching-source and trigger-unknown-error debug reports when trigger 252 * doesn't have related source. 253 */ scheduleTriggerNoMatchingSourceDebugReport( Trigger trigger, IMeasurementDao dao, DebugReportApi.Type type)254 public void scheduleTriggerNoMatchingSourceDebugReport( 255 Trigger trigger, IMeasurementDao dao, DebugReportApi.Type type) 256 throws DatastoreException { 257 if (!isTriggerReportAllowed(trigger, type, /* source= */ null)) { 258 return; 259 } 260 Pair<UnsignedLong, UnsignedLong> debugKeyPair = 261 new DebugKeyAccessor(dao).getDebugKeysForVerboseTriggerDebugReport(null, trigger); 262 scheduleReport( 263 type, 264 generateTriggerDebugReportBody(null, trigger, null, debugKeyPair, true), 265 trigger.getEnrollmentId(), 266 trigger.getRegistrationOrigin(), 267 trigger.getRegistrant(), 268 dao); 269 } 270 271 /** Schedules Trigger Debug Reports with/without limit, pass in Type for different types. */ scheduleTriggerDebugReport( Source source, Trigger trigger, @Nullable String limit, IMeasurementDao dao, DebugReportApi.Type type)272 public void scheduleTriggerDebugReport( 273 Source source, 274 Trigger trigger, 275 @Nullable String limit, 276 IMeasurementDao dao, 277 DebugReportApi.Type type) 278 throws DatastoreException { 279 if (!isTriggerReportAllowed(trigger, type, source)) { 280 return; 281 } 282 Pair<UnsignedLong, UnsignedLong> debugKeyPair = 283 new DebugKeyAccessor(dao).getDebugKeysForVerboseTriggerDebugReport(source, trigger); 284 scheduleReport( 285 type, 286 generateTriggerDebugReportBody(source, trigger, limit, debugKeyPair, false), 287 source.getEnrollmentId(), 288 trigger.getRegistrationOrigin(), 289 source.getRegistrant(), 290 dao); 291 } 292 293 /** 294 * Schedules Trigger Debug Reports with/without limit and bucket, pass in Type for different 295 * types. 296 */ scheduleTriggerDebugReport( Source source, Trigger trigger, String limit, String budgetName, IMeasurementDao dao, DebugReportApi.Type type)297 public void scheduleTriggerDebugReport( 298 Source source, 299 Trigger trigger, 300 String limit, 301 String budgetName, 302 IMeasurementDao dao, 303 DebugReportApi.Type type) 304 throws DatastoreException { 305 Objects.requireNonNull(source, "source cannot be null"); 306 Objects.requireNonNull(trigger, "trigger cannot be null"); 307 Objects.requireNonNull(limit, "limit cannot be null"); 308 Objects.requireNonNull(budgetName, "budgetName cannot be null"); 309 Objects.requireNonNull(dao, "dao cannot be null"); 310 Objects.requireNonNull(type, "type cannot be null"); 311 if (!isTriggerReportAllowed(trigger, type, source)) { 312 return; 313 } 314 Pair<UnsignedLong, UnsignedLong> debugKeyPair = 315 new DebugKeyAccessor(dao).getDebugKeysForVerboseTriggerDebugReport(source, trigger); 316 scheduleReport( 317 type, 318 generateTriggerDebugReportBody( 319 source, 320 trigger, 321 limit, 322 budgetName, 323 debugKeyPair, 324 /* is TriggerNoMatchingSource= */ false), 325 source.getEnrollmentId(), 326 trigger.getRegistrationOrigin(), 327 source.getRegistrant(), 328 dao); 329 } 330 331 /** 332 * Schedules Trigger Debug Report with all body fields, Used for trigger-low-priority report and 333 * trigger-event-excessive-reports. 334 */ scheduleTriggerDebugReportWithAllFields( Source source, Trigger trigger, UnsignedLong triggerData, IMeasurementDao dao, DebugReportApi.Type type)335 public void scheduleTriggerDebugReportWithAllFields( 336 Source source, 337 Trigger trigger, 338 UnsignedLong triggerData, 339 IMeasurementDao dao, 340 DebugReportApi.Type type) 341 throws DatastoreException { 342 if (!isTriggerReportAllowed(trigger, type, source)) { 343 return; 344 } 345 Pair<UnsignedLong, UnsignedLong> debugKeyPair = 346 new DebugKeyAccessor(dao).getDebugKeysForVerboseTriggerDebugReport(source, trigger); 347 scheduleReport( 348 type, 349 generateTriggerDebugReportBodyWithAllFields( 350 source, trigger, triggerData, debugKeyPair), 351 source.getEnrollmentId(), 352 trigger.getRegistrationOrigin(), 353 source.getRegistrant(), 354 dao); 355 } 356 357 /** Schedules the Source Destination limit type Debug Report */ scheduleSourceDestinationLimitDebugReport( Source source, String limit, Type type, IMeasurementDao dao)358 private void scheduleSourceDestinationLimitDebugReport( 359 Source source, String limit, Type type, IMeasurementDao dao) { 360 if (isSourceDebugFlagDisabled(Type.SOURCE_DESTINATION_LIMIT, source)) { 361 return; 362 } 363 if (isAdTechNotOptIn( 364 source.isDebugReporting(), 365 Type.SOURCE_DESTINATION_LIMIT, 366 source, 367 /* trigger= */ null)) { 368 return; 369 } 370 try { 371 JSONObject body = new JSONObject(); 372 body.put(Body.SOURCE_EVENT_ID, source.getEventId().toString()); 373 body.put(Body.ATTRIBUTION_DESTINATION, generateSourceDestinations(source)); 374 body.put(Body.SOURCE_SITE, generateSourceSite(source)); 375 body.put(Body.LIMIT, limit); 376 if (getAdIdPermissionFromSource(source) == PermissionState.GRANTED 377 || getArDebugPermissionFromSource(source) == PermissionState.GRANTED) { 378 body.put(Body.SOURCE_DEBUG_KEY, source.getDebugKey()); 379 } 380 scheduleReport( 381 type, 382 body, 383 source.getEnrollmentId(), 384 source.getRegistrationOrigin(), 385 source.getRegistrant(), 386 dao); 387 } catch (JSONException e) { 388 LoggerFactory.getMeasurementLogger() 389 .e( 390 e, 391 "DebugReportApi: JSON error in debug report %s. Enrollment ID: %s", 392 type, 393 source.getEnrollmentId()); 394 } 395 } 396 397 /** Schedule header parsing and validation errors verbose debug reports. */ scheduleHeaderErrorReport( Uri topOrigin, Uri registrationOrigin, Uri registrant, String headerName, String enrollmentId, @Nullable String originalHeader, IMeasurementDao dao)398 public void scheduleHeaderErrorReport( 399 Uri topOrigin, 400 Uri registrationOrigin, 401 Uri registrant, 402 String headerName, 403 String enrollmentId, 404 @Nullable String originalHeader, 405 IMeasurementDao dao) { 406 try { 407 JSONObject body = new JSONObject(); 408 body.put(Body.CONTEXT_SITE, topOrigin); 409 body.put(Body.HEADER, headerName); 410 body.put(Body.VALUE, originalHeader == null ? "null" : originalHeader); 411 scheduleReport( 412 Type.HEADER_PARSING_ERROR, 413 body, 414 enrollmentId, 415 registrationOrigin, 416 registrant, 417 dao); 418 } catch (JSONException e) { 419 LoggerFactory.getMeasurementLogger() 420 .e( 421 e, 422 "DebugReportApi: JSON error in debug report %s. Enrollment ID: %s", 423 Type.HEADER_PARSING_ERROR, 424 enrollmentId); 425 } 426 } 427 428 /** 429 * Schedules the Source Debug Report to be sent 430 * 431 * @param source The source 432 * @param type The type of the debug report 433 * @param additionalBodyParams Additional parameters to add to the body of the debug report 434 * @param dao Measurement DAO 435 */ scheduleSourceReport( Source source, Type type, @Nullable Map<String, Object> additionalBodyParams, IMeasurementDao dao)436 public void scheduleSourceReport( 437 Source source, 438 Type type, 439 @Nullable Map<String, Object> additionalBodyParams, 440 IMeasurementDao dao) { 441 Objects.requireNonNull(source, "source cannot be null"); 442 Objects.requireNonNull(type, "type cannot be null"); 443 Objects.requireNonNull(dao, "dao cannot be null"); 444 String enrollmentId = source.getEnrollmentId(); 445 Objects.requireNonNull(enrollmentId, "source enrollmentId cannot be null"); 446 JSONObject body = generateSourceDebugReportBody(source, additionalBodyParams); 447 Objects.requireNonNull(body, "debug report body cannot be null"); 448 449 if (isSourceDebugFlagDisabled(type, source)) { 450 return; 451 } 452 if (isAdTechNotOptIn(source.isDebugReporting(), type, source, /* trigger= */ null)) { 453 return; 454 } 455 if (!isSourcePermissionGranted(source)) { 456 LoggerFactory.getMeasurementLogger() 457 .d( 458 "DebugReportApi: Skipping source debug report %s. Source ID: %s, Source" 459 + " Event ID: %s, Enrollment ID: %s", 460 type, source.getId(), source.getEventId(), source.getEnrollmentId()); 461 return; 462 } 463 if (body.length() == 0) { 464 LoggerFactory.getMeasurementLogger() 465 .d( 466 "DebugReportApi: Empty debug report found %s. Source ID: %s, Source" 467 + " Event ID: %s, Enrollment ID: %s", 468 type, source.getId(), source.getEventId(), source.getEnrollmentId()); 469 return; 470 } 471 if (enrollmentId.isEmpty()) { 472 LoggerFactory.getMeasurementLogger() 473 .d( 474 "DebugReportApi: Empty enrollment found %s. Source ID: %s, Source Event" 475 + " ID: %s", 476 type, source.getId(), source.getEventId()); 477 return; 478 } 479 480 DebugReport debugReport = 481 new DebugReport.Builder() 482 .setId(UUID.randomUUID().toString()) 483 .setType(type.getValue()) 484 .setBody(body) 485 .setEnrollmentId(enrollmentId) 486 .setRegistrationOrigin(source.getRegistrationOrigin()) 487 .setInsertionTime(System.currentTimeMillis()) 488 .setRegistrant(source.getRegistrant()) 489 .build(); 490 try { 491 dao.insertDebugReport(debugReport); 492 } catch (DatastoreException e) { 493 LoggerFactory.getMeasurementLogger() 494 .e( 495 e, 496 "DebugReportApi: Failed to insert source debug report %s. Enrollment" 497 + " ID: %s", 498 type, 499 enrollmentId); 500 LoggerFactory.getMeasurementLogger() 501 .d("Source ID: %s, Source Event ID: %s", source.getId(), source.getEventId()); 502 } 503 504 LoggerFactory.getMeasurementLogger() 505 .d( 506 "DebugReportApi: Successfully scheduled source debug report %s. Report ID:" 507 + " %s, Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 508 type, 509 debugReport.getId(), 510 enrollmentId, 511 source.getId(), 512 source.getEventId()); 513 VerboseDebugReportingJobService.scheduleIfNeeded(mContext, /* forceSchedule= */ false); 514 } 515 516 /** 517 * Schedules the Debug Report to be sent 518 * 519 * @param type The type of the debug report 520 * @param body The body of the debug report 521 * @param enrollmentId Ad Tech enrollment ID 522 * @param registrationOrigin Reporting origin of the report 523 * @param dao Measurement DAO 524 * @param registrant App Registrant 525 */ scheduleReport( Type type, JSONObject body, String enrollmentId, Uri registrationOrigin, @Nullable Uri registrant, IMeasurementDao dao)526 private void scheduleReport( 527 Type type, 528 JSONObject body, 529 String enrollmentId, 530 Uri registrationOrigin, 531 @Nullable Uri registrant, 532 IMeasurementDao dao) { 533 Objects.requireNonNull(type); 534 Objects.requireNonNull(body); 535 Objects.requireNonNull(enrollmentId); 536 Objects.requireNonNull(dao); 537 if (body.length() == 0) { 538 LoggerFactory.getMeasurementLogger().d("Empty debug report found %s", type); 539 return; 540 } 541 if (enrollmentId.isEmpty()) { 542 LoggerFactory.getMeasurementLogger().d("Empty enrollment found %s", type); 543 return; 544 } 545 546 DebugReport debugReport = 547 new DebugReport.Builder() 548 .setId(UUID.randomUUID().toString()) 549 .setType(type.getValue()) 550 .setBody(body) 551 .setEnrollmentId(enrollmentId) 552 .setRegistrationOrigin(registrationOrigin) 553 .setInsertionTime(System.currentTimeMillis()) 554 .setRegistrant(registrant) 555 .build(); 556 try { 557 dao.insertDebugReport(debugReport); 558 } catch (DatastoreException e) { 559 LoggerFactory.getMeasurementLogger() 560 .e( 561 e, 562 "DebugReportApi: Failed to insert debug report %s. Enrollment ID: %s", 563 type, 564 enrollmentId); 565 } 566 567 LoggerFactory.getMeasurementLogger() 568 .d( 569 "DebugReportApi: Successfully scheduled debug report %s. Report ID: %s," 570 + " Enrollment ID: %s", 571 type, debugReport.getId(), enrollmentId); 572 VerboseDebugReportingJobService.scheduleIfNeeded(mContext, /* forceSchedule= */ false); 573 } 574 575 /** Get AdIdPermission State from Source */ getAdIdPermissionFromSource(Source source)576 private PermissionState getAdIdPermissionFromSource(Source source) { 577 if (source.getPublisherType() == EventSurfaceType.APP) { 578 if (source.hasAdIdPermission()) { 579 return PermissionState.GRANTED; 580 } else { 581 LoggerFactory.getMeasurementLogger() 582 .d( 583 "DebugReportApi: Source doesn't have AdId permission. Source ID:" 584 + " %s, Source Event ID: %s, Enrollment ID: %s", 585 source.getId(), source.getEventId(), source.getEnrollmentId()); 586 return PermissionState.DENIED; 587 } 588 } 589 return PermissionState.NONE; 590 } 591 592 /** Get ArDebugPermission State from Source */ getArDebugPermissionFromSource(Source source)593 private PermissionState getArDebugPermissionFromSource(Source source) { 594 if (source.getPublisherType() == EventSurfaceType.WEB) { 595 if (source.hasArDebugPermission()) { 596 return PermissionState.GRANTED; 597 } else { 598 LoggerFactory.getMeasurementLogger() 599 .d( 600 "DebugReportApi: Source doesn't have ArDebug permission. Source ID:" 601 + " %s, Source Event ID: %s, Enrollment ID: %s", 602 source.getId(), source.getEventId(), source.getEnrollmentId()); 603 return PermissionState.DENIED; 604 } 605 } 606 return PermissionState.NONE; 607 } 608 getAdIdPermissionFromTrigger(Trigger trigger)609 private PermissionState getAdIdPermissionFromTrigger(Trigger trigger) { 610 if (trigger.getDestinationType() == EventSurfaceType.APP) { 611 if (trigger.hasAdIdPermission()) { 612 return PermissionState.GRANTED; 613 } else { 614 LoggerFactory.getMeasurementLogger() 615 .d( 616 "DebugReportApi: Trigger doesn't have AdId permission. Trigger ID:" 617 + " %s, Enrollment ID: %s", 618 trigger.getId(), trigger.getEnrollmentId()); 619 return PermissionState.DENIED; 620 } 621 } 622 return PermissionState.NONE; 623 } 624 getArDebugPermissionFromTrigger(Trigger trigger)625 private PermissionState getArDebugPermissionFromTrigger(Trigger trigger) { 626 if (trigger.getDestinationType() == EventSurfaceType.WEB) { 627 if (trigger.hasArDebugPermission()) { 628 return PermissionState.GRANTED; 629 } else { 630 LoggerFactory.getMeasurementLogger() 631 .d( 632 "DebugReportApi: Trigger doesn't have ArDebug permission. Trigger" 633 + " ID: %s, Enrollment ID: %s", 634 trigger.getId(), trigger.getEnrollmentId()); 635 return PermissionState.DENIED; 636 } 637 } 638 return PermissionState.NONE; 639 } 640 641 /** 642 * Check AdId and ArDebug permissions for both source and trigger. Return true if all of them 643 * are not in {@link PermissionState#DENIED} state. 644 */ isSourceAndTriggerPermissionsGranted(@ullable Source source, Trigger trigger)645 private boolean isSourceAndTriggerPermissionsGranted(@Nullable Source source, Trigger trigger) { 646 return source == null 647 ? isTriggerPermissionGranted(trigger) 648 : (isSourcePermissionGranted(source) && isTriggerPermissionGranted(trigger)); 649 } 650 isSourcePermissionGranted(Source source)651 private boolean isSourcePermissionGranted(Source source) { 652 return getAdIdPermissionFromSource(source) != PermissionState.DENIED 653 && getArDebugPermissionFromSource(source) != PermissionState.DENIED; 654 } 655 isTriggerPermissionGranted(Trigger trigger)656 private boolean isTriggerPermissionGranted(Trigger trigger) { 657 return getAdIdPermissionFromTrigger(trigger) != PermissionState.DENIED 658 && getArDebugPermissionFromTrigger(trigger) != PermissionState.DENIED; 659 } 660 661 /** Get is Ad tech not op-in and log */ isAdTechNotOptIn( boolean optIn, DebugReportApi.Type type, @Nullable Source source, @Nullable Trigger trigger)662 private boolean isAdTechNotOptIn( 663 boolean optIn, 664 DebugReportApi.Type type, 665 @Nullable Source source, 666 @Nullable Trigger trigger) { 667 if (!optIn) { 668 String enrollmentId = 669 source != null ? source.getEnrollmentId() : trigger.getEnrollmentId(); 670 LoggerFactory.getMeasurementLogger() 671 .d( 672 "DebugReportApi: Ad-tech not opt-in. Skipping debug report %s." 673 + " Enrollment ID: %s%s%s", 674 type, 675 enrollmentId, 676 maybeGetSourceInfo(source), 677 maybeGetTriggerInfo(trigger)); 678 } 679 return !optIn; 680 } 681 682 /** Generates source debug report body */ generateSourceDebugReportBody( Source source, @Nullable Map<String, Object> additionalBodyParams)683 private JSONObject generateSourceDebugReportBody( 684 Source source, @Nullable Map<String, Object> additionalBodyParams) { 685 JSONObject body = new JSONObject(); 686 try { 687 body.put(Body.SOURCE_EVENT_ID, source.getEventId().toString()); 688 body.put(Body.ATTRIBUTION_DESTINATION, generateSourceDestinations(source)); 689 body.put(Body.SOURCE_SITE, generateSourceSite(source)); 690 body.put(Body.SOURCE_DEBUG_KEY, source.getDebugKey()); 691 if (additionalBodyParams != null) { 692 for (Map.Entry<String, Object> entry : additionalBodyParams.entrySet()) { 693 body.put(entry.getKey(), entry.getValue()); 694 } 695 } 696 697 } catch (JSONException e) { 698 LoggerFactory.getMeasurementLogger() 699 .e(e, "DebugReportApi: JSON error while generating source debug report body."); 700 LoggerFactory.getMeasurementLogger() 701 .d( 702 "Source ID: %s, Source Event ID: %s, Enrollment ID: %s", 703 source.getId(), source.getEventId(), source.getEnrollmentId()); 704 } 705 return body; 706 } 707 generateSourceDestinations(Source source)708 private static Object generateSourceDestinations(Source source) throws JSONException { 709 List<Uri> destinations = new ArrayList<>(); 710 Optional.ofNullable(source.getAppDestinations()).ifPresent(destinations::addAll); 711 List<Uri> webDestinations = source.getWebDestinations(); 712 if (webDestinations != null) { 713 for (Uri webDestination : webDestinations) { 714 Optional<Uri> webUri = WebAddresses.topPrivateDomainAndScheme(webDestination); 715 webUri.ifPresent(destinations::add); 716 } 717 } 718 return ReportUtil.serializeAttributionDestinations(destinations); 719 } 720 generateSourceSite(Source source)721 private static Uri generateSourceSite(Source source) { 722 if (source.getPublisherType() == EventSurfaceType.APP) { 723 return source.getPublisher(); 724 } else { 725 return WebAddresses.topPrivateDomainAndScheme(source.getPublisher()).orElse(null); 726 } 727 } 728 729 /** Generates trigger debug report body */ generateTriggerDebugReportBody( @ullable Source source, @NonNull Trigger trigger, @Nullable String limit, @NonNull Pair<UnsignedLong, UnsignedLong> debugKeyPair, boolean isTriggerNoMatchingSource)730 private JSONObject generateTriggerDebugReportBody( 731 @Nullable Source source, 732 @NonNull Trigger trigger, 733 @Nullable String limit, 734 @NonNull Pair<UnsignedLong, UnsignedLong> debugKeyPair, 735 boolean isTriggerNoMatchingSource) { 736 JSONObject body = new JSONObject(); 737 try { 738 body.put(Body.ATTRIBUTION_DESTINATION, trigger.getAttributionDestinationBaseUri()); 739 body.put(Body.TRIGGER_DEBUG_KEY, debugKeyPair.second); 740 if (isTriggerNoMatchingSource) { 741 return body; 742 } 743 body.put(Body.LIMIT, limit); 744 body.put(Body.SOURCE_DEBUG_KEY, debugKeyPair.first); 745 body.put(Body.SOURCE_EVENT_ID, source.getEventId().toString()); 746 body.put(Body.SOURCE_SITE, generateSourceSite(source)); 747 } catch (JSONException e) { 748 LoggerFactory.getMeasurementLogger() 749 .e(e, "DebugReportApi: JSON error while generating trigger debug report body."); 750 LoggerFactory.getMeasurementLogger() 751 .d( 752 "Trigger ID: %s, Enrollment ID: %s%s", 753 trigger.getId(), trigger.getEnrollmentId(), maybeGetSourceInfo(source)); 754 } 755 return body; 756 } 757 758 /** Generates trigger debug report body */ generateTriggerDebugReportBody( Source source, Trigger trigger, String limit, String budgetName, Pair<UnsignedLong, UnsignedLong> debugKeyPair, boolean isTriggerNoMatchingSource)759 private JSONObject generateTriggerDebugReportBody( 760 Source source, 761 Trigger trigger, 762 String limit, 763 String budgetName, 764 Pair<UnsignedLong, UnsignedLong> debugKeyPair, 765 boolean isTriggerNoMatchingSource) { 766 JSONObject body = new JSONObject(); 767 try { 768 body.put(Body.ATTRIBUTION_DESTINATION, trigger.getAttributionDestinationBaseUri()); 769 body.put(Body.TRIGGER_DEBUG_KEY, debugKeyPair.second); 770 if (isTriggerNoMatchingSource) { 771 return body; 772 } 773 body.put(Body.LIMIT, limit); 774 body.put(Body.NAME, budgetName); 775 body.put(Body.SOURCE_DEBUG_KEY, debugKeyPair.first); 776 body.put(Body.SOURCE_EVENT_ID, source.getEventId().toString()); 777 body.put(Body.SOURCE_SITE, generateSourceSite(source)); 778 } catch (JSONException e) { 779 LoggerFactory.getMeasurementLogger() 780 .e(e, "DebugReportApi: JSON error while generating trigger debug report body."); 781 LoggerFactory.getMeasurementLogger() 782 .d( 783 "Trigger ID: %s, Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 784 trigger.getId(), 785 trigger.getEnrollmentId(), 786 source.getId(), 787 source.getEventId()); 788 } 789 return body; 790 } 791 792 /** 793 * Generates trigger debug report body with all fields in event-level attribution report. Used 794 * for trigger-low-priority, trigger-event-excessive-reports debug reports. 795 */ generateTriggerDebugReportBodyWithAllFields( @onNull Source source, @NonNull Trigger trigger, @Nullable UnsignedLong triggerData, @NonNull Pair<UnsignedLong, UnsignedLong> debugKeyPair)796 private JSONObject generateTriggerDebugReportBodyWithAllFields( 797 @NonNull Source source, 798 @NonNull Trigger trigger, 799 @Nullable UnsignedLong triggerData, 800 @NonNull Pair<UnsignedLong, UnsignedLong> debugKeyPair) { 801 JSONObject body = new JSONObject(); 802 try { 803 body.put(Body.ATTRIBUTION_DESTINATION, trigger.getAttributionDestinationBaseUri()); 804 body.put( 805 Body.SCHEDULED_REPORT_TIME, 806 String.valueOf( 807 TimeUnit.MILLISECONDS.toSeconds( 808 mEventReportWindowCalcDelegate.getReportingTime( 809 source, 810 trigger.getTriggerTime(), 811 trigger.getDestinationType())))); 812 body.put(Body.SOURCE_EVENT_ID, source.getEventId()); 813 body.put(Body.SOURCE_TYPE, source.getSourceType().getValue()); 814 body.put( 815 Body.RANDOMIZED_TRIGGER_RATE, 816 mSourceNoiseHandler.getRandomizedTriggerRate(source)); 817 if (triggerData != null) { 818 body.put(Body.TRIGGER_DATA, triggerData.toString()); 819 } 820 if (debugKeyPair.first != null) { 821 body.put(Body.SOURCE_DEBUG_KEY, debugKeyPair.first); 822 } 823 if (debugKeyPair.second != null) { 824 body.put(Body.TRIGGER_DEBUG_KEY, debugKeyPair.second); 825 } 826 } catch (JSONException e) { 827 LoggerFactory.getMeasurementLogger() 828 .e( 829 e, 830 "DebugReportApi: JSON error while generating trigger debug report body" 831 + " with all fields."); 832 LoggerFactory.getMeasurementLogger() 833 .d( 834 "Trigger ID: %s, Enrollment ID: %s, Source ID: %s, Source Event ID: %s", 835 trigger.getId(), 836 trigger.getEnrollmentId(), 837 source.getId(), 838 source.getEventId()); 839 } 840 return body; 841 } 842 843 /** Checks flags for source debug reports. */ isSourceDebugFlagDisabled(DebugReportApi.Type type, Source source)844 private boolean isSourceDebugFlagDisabled(DebugReportApi.Type type, Source source) { 845 if (!mFlags.getMeasurementEnableDebugReport() 846 || !mFlags.getMeasurementEnableSourceDebugReport()) { 847 LoggerFactory.getMeasurementLogger() 848 .d( 849 "DebugReportApi: Source flag is disabled for %s debug report. Source" 850 + " ID: %s, Source Event ID: %s, Enrollment ID: %s", 851 type, source.getId(), source.getEventId(), source.getEnrollmentId()); 852 return true; 853 } 854 return false; 855 } 856 857 /** Checks flags for trigger debug reports. */ isTriggerDebugFlagDisabled( DebugReportApi.Type type, Trigger trigger, @Nullable Source source)858 private boolean isTriggerDebugFlagDisabled( 859 DebugReportApi.Type type, Trigger trigger, @Nullable Source source) { 860 if (!mFlags.getMeasurementEnableDebugReport() 861 || !mFlags.getMeasurementEnableTriggerDebugReport()) { 862 LoggerFactory.getMeasurementLogger() 863 .d( 864 "DebugReportApi: Trigger flag is disabled for %s debug report. Trigger" 865 + " ID: %s, Enrollment ID: %s%s", 866 type, 867 trigger.getId(), 868 trigger.getEnrollmentId(), 869 maybeGetSourceInfo(source)); 870 return true; 871 } 872 return false; 873 } 874 maybeGetSourceInfo(@ullable Source source)875 private String maybeGetSourceInfo(@Nullable Source source) { 876 return source != null 877 ? String.format( 878 ", Source ID: %s, Source Event ID: %s", source.getId(), source.getEventId()) 879 : ""; 880 } 881 maybeGetTriggerInfo(@ullable Trigger trigger)882 private String maybeGetTriggerInfo(@Nullable Trigger trigger) { 883 return trigger != null ? String.format(", Trigger ID: %s", trigger.getId()) : ""; 884 } 885 } 886