• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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