| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.adservices.service.measurement.attribution; |
| |
| import static com.android.adservices.service.Flags.MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS; |
| import static com.android.adservices.service.Flags.MEASUREMENT_MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNull; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.anyInt; |
| import static org.mockito.ArgumentMatchers.anyLong; |
| import static org.mockito.ArgumentMatchers.anyString; |
| import static org.mockito.ArgumentMatchers.eq; |
| import static org.mockito.Mockito.doReturn; |
| import static org.mockito.Mockito.doThrow; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.spy; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| import android.content.Context; |
| import android.net.Uri; |
| import android.util.Pair; |
| |
| import androidx.test.core.app.ApplicationProvider; |
| |
| import com.android.adservices.LogUtil; |
| import com.android.adservices.common.WebUtil; |
| import com.android.adservices.data.measurement.DatastoreException; |
| import com.android.adservices.data.measurement.DatastoreManager; |
| import com.android.adservices.data.measurement.IMeasurementDao; |
| import com.android.adservices.data.measurement.ITransaction; |
| import com.android.adservices.service.AdServicesConfig; |
| import com.android.adservices.service.Flags; |
| import com.android.adservices.service.measurement.Attribution; |
| import com.android.adservices.service.measurement.AttributionConfig; |
| import com.android.adservices.service.measurement.EventReport; |
| import com.android.adservices.service.measurement.EventSurfaceType; |
| import com.android.adservices.service.measurement.PrivacyParams; |
| import com.android.adservices.service.measurement.Source; |
| import com.android.adservices.service.measurement.SourceFixture; |
| import com.android.adservices.service.measurement.Trigger; |
| import com.android.adservices.service.measurement.TriggerFixture; |
| import com.android.adservices.service.measurement.TriggerSpecs; |
| import com.android.adservices.service.measurement.aggregation.AggregateAttributionData; |
| import com.android.adservices.service.measurement.aggregation.AggregateHistogramContribution; |
| import com.android.adservices.service.measurement.aggregation.AggregateReport; |
| import com.android.adservices.service.measurement.noising.SourceNoiseHandler; |
| import com.android.adservices.service.measurement.reporting.DebugReportApi; |
| import com.android.adservices.service.measurement.reporting.EventReportWindowCalcDelegate; |
| import com.android.adservices.service.measurement.util.UnsignedLong; |
| import com.android.adservices.service.stats.AdServicesLogger; |
| import com.android.adservices.service.stats.AdServicesLoggerImpl; |
| import com.android.adservices.service.stats.MeasurementAttributionStats; |
| import com.android.adservices.shared.errorlogging.AdServicesErrorLogger; |
| import com.android.modules.utils.testing.TestableDeviceConfig; |
| |
| import org.json.JSONArray; |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| import org.junit.Before; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.Mock; |
| import org.mockito.junit.MockitoJUnitRunner; |
| |
| import java.math.BigInteger; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.UUID; |
| import java.util.concurrent.TimeUnit; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Unit test for {@link AttributionJobHandler} |
| */ |
| @RunWith(MockitoJUnitRunner.class) |
| public class AttributionJobHandlerTest { |
| @Rule |
| public final TestableDeviceConfig.TestableDeviceConfigRule mDeviceConfigRule = |
| new TestableDeviceConfig.TestableDeviceConfigRule(); |
| |
| private static final Long PENULTIMATE_INT_AS_LONG = |
| Integer.valueOf(Integer.MAX_VALUE - 1).longValue(); |
| private static final long SOURCE_TIME = 1690000000000L; |
| private static final long TRIGGER_TIME = 1690000001000L; |
| private static final long EXPIRY_TIME = 1692592000000L; |
| private static final long LOOKBACK_WINDOW = 1000L; |
| private static final Context sContext = ApplicationProvider.getApplicationContext(); |
| private static final Uri APP_DESTINATION = Uri.parse("android-app://com.example.app"); |
| private static final Uri WEB_DESTINATION = WebUtil.validUri("https://web.example.test"); |
| private static final Uri PUBLISHER = Uri.parse("android-app://publisher.app"); |
| private static final Uri REGISTRATION_URI = WebUtil.validUri("https://subdomain.example.test"); |
| private static final UnsignedLong SOURCE_DEBUG_KEY = new UnsignedLong(111111L); |
| private static final UnsignedLong TRIGGER_DEBUG_KEY = new UnsignedLong(222222L); |
| private static final String TRIGGER_ID = "triggerId1"; |
| private static final String EVENT_TRIGGERS = |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"source_type\": [\"event\"],\n" |
| + " \"key_1\": [\"value_1\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n"; |
| |
| private static final String AGGREGATE_DEDUPLICATION_KEYS_1 = |
| "[{\"deduplication_key\": \"" + 10 + "\"" + " }" + "]"; |
| |
| private static Trigger createAPendingTriggerEventScopeOnly() { |
| return TriggerFixture.getValidTriggerBuilder() |
| .setId(TRIGGER_ID) |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers(EVENT_TRIGGERS) |
| .build(); |
| } |
| |
| DatastoreManager mDatastoreManager; |
| |
| AttributionJobHandler mHandler; |
| |
| EventReportWindowCalcDelegate mEventReportWindowCalcDelegate; |
| |
| SourceNoiseHandler mSourceNoiseHandler; |
| |
| @Mock |
| IMeasurementDao mMeasurementDao; |
| |
| @Mock |
| ITransaction mTransaction; |
| |
| @Mock Flags mFlags; |
| |
| @Mock AdServicesLogger mLogger; |
| @Mock AdServicesErrorLogger mErrorLogger; |
| @Mock DebugReportApi mDebugReportApi; |
| |
| class FakeDatastoreManager extends DatastoreManager { |
| |
| FakeDatastoreManager() { |
| super(mErrorLogger); |
| } |
| |
| @Override |
| public ITransaction createNewTransaction() { |
| return mTransaction; |
| } |
| |
| @Override |
| public IMeasurementDao getMeasurementDao() { |
| return mMeasurementDao; |
| } |
| |
| @Override |
| protected int getDataStoreVersion() { |
| return 0; |
| } |
| } |
| |
| @Before |
| public void before() { |
| mDatastoreManager = new FakeDatastoreManager(); |
| mEventReportWindowCalcDelegate = spy(new EventReportWindowCalcDelegate(mFlags)); |
| mSourceNoiseHandler = spy(new SourceNoiseHandler(mFlags)); |
| mLogger = spy(AdServicesLoggerImpl.getInstance()); |
| mHandler = |
| new AttributionJobHandler( |
| mDatastoreManager, |
| mFlags, |
| mDebugReportApi, |
| mEventReportWindowCalcDelegate, |
| mSourceNoiseHandler, |
| mLogger, |
| new XnaSourceCreator(mFlags)); |
| when(mFlags.getMeasurementEnableXNA()).thenReturn(false); |
| when(mFlags.getMeasurementEnableScopedAttributionRateLimit()) |
| .thenReturn(Flags.MEASUREMENT_ENABLE_SCOPED_ATTRIBUTION_RATE_LIMIT); |
| when(mFlags.getMeasurementMaxAttributionPerRateLimitWindow()) |
| .thenReturn(Flags.MEASUREMENT_MAX_ATTRIBUTION_PER_RATE_LIMIT_WINDOW); |
| when(mFlags.getMeasurementMaxEventAttributionPerRateLimitWindow()) |
| .thenReturn(Flags.MEASUREMENT_MAX_EVENT_ATTRIBUTION_PER_RATE_LIMIT_WINDOW); |
| when(mFlags.getMeasurementMaxAggregateAttributionPerRateLimitWindow()) |
| .thenReturn(Flags.MEASUREMENT_MAX_AGGREGATE_ATTRIBUTION_PER_RATE_LIMIT_WINDOW); |
| when(mFlags.getMeasurementMaxDistinctEnrollmentsInAttribution()) |
| .thenReturn(Flags.MEASUREMENT_MAX_DISTINCT_ENROLLMENTS_IN_ATTRIBUTION); |
| when(mFlags.getMeasurementEnableAraDeduplicationAlignmentV1()).thenReturn(true); |
| when(mFlags.getMeasurementMaxAttributionsPerInvocation()) |
| .thenReturn(Flags.DEFAULT_MEASUREMENT_MAX_ATTRIBUTIONS_PER_INVOCATION); |
| when(mFlags.getMeasurementMaxEventReportsPerDestination()) |
| .thenReturn(Flags.MEASUREMENT_MAX_EVENT_REPORTS_PER_DESTINATION); |
| when(mFlags.getMeasurementMaxAggregateReportsPerDestination()) |
| .thenReturn(Flags.MEASUREMENT_MAX_AGGREGATE_REPORTS_PER_DESTINATION); |
| when(mFlags.getMeasurementNullAggReportRateInclSourceRegistrationTime()).thenReturn(0f); |
| when(mFlags.getMeasurementMinEventReportDelayMillis()) |
| .thenReturn(Flags.MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS); |
| } |
| |
| @Test |
| public void shouldIgnoreNonPendingTrigger() throws DatastoreException { |
| Trigger trigger = TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.IGNORED).build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao).getTrigger(trigger.getId()); |
| verify(mMeasurementDao, never()).updateTriggerStatus(any(), anyInt()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldIgnoreIfNoSourcesFound() throws DatastoreException { |
| Trigger trigger = createAPendingTriggerEventScopeOnly(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(new ArrayList<>()); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldRejectBasedOnDedupKey() throws DatastoreException { |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"source_type\": [\"event\"],\n" |
| + " \"key_1\": [\"value_1\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .build(); |
| String attributionStatus = getAttributionStatus( |
| List.of("triggerId2", "triggerId3"), |
| List.of("1", "2"), |
| List.of("1", "2")); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventAttributionStatus(attributionStatus) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldRejectBasedOnDedupKey_dedupAlignFlagOff() throws DatastoreException { |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"source_type\": [\"event\"],\n" |
| + " \"key_1\": [\"value_1\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventReportDedupKeys( |
| Arrays.asList(new UnsignedLong(1L), new UnsignedLong(2L))) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mFlags.getMeasurementEnableAraDeduplicationAlignmentV1()).thenReturn(false); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldNotCreateEventReportAfterEventReportWindow() |
| throws DatastoreException { |
| long eventTime = System.currentTimeMillis(); |
| long triggerTime = eventTime + TimeUnit.DAYS.toMillis(5); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[{\"trigger_data\":\"1\"," |
| + "\"filters\":[{\"source_type\": [\"event\"]}]" |
| + "}]") |
| .setTriggerTime(triggerTime) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source1") |
| .setEventId(new UnsignedLong(1L)) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(eventTime) |
| .setEventReportWindow(triggerTime - 1) |
| .build(); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldNotCreateEventReportAfterEventReportWindow_secondTrigger() |
| throws DatastoreException, JSONException { |
| long eventTime = System.currentTimeMillis(); |
| long triggerTime = eventTime + TimeUnit.DAYS.toMillis(5); |
| Trigger trigger1 = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers("[{\"trigger_data\":\"1\"}]") |
| .setTriggerTime(triggerTime) |
| .build(); |
| Trigger trigger2 = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId2") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers("[{\"trigger_data\":\"0\"}]") |
| .setTriggerTime(triggerTime + 1L) |
| .build(); |
| List<Trigger> triggers = new ArrayList<>(); |
| triggers.add(trigger1); |
| triggers.add(trigger2); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(eventTime) |
| .setExpiryTime(eventTime + TimeUnit.DAYS.toMillis(30)) |
| .setEventReportWindow(triggerTime + 1L) |
| .build(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Arrays.asList(trigger1.getId(), trigger2.getId())); |
| when(mMeasurementDao.getTrigger(trigger1.getId())).thenReturn(trigger1); |
| when(mMeasurementDao.getTrigger(trigger2.getId())).thenReturn(trigger2); |
| when(mMeasurementDao.getMatchingActiveSources(trigger1)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getMatchingActiveSources(trigger2)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| |
| assertTrue(mHandler.performPendingAttributions()); |
| // Verify trigger status updates. |
| verify(mMeasurementDao).updateTriggerStatus( |
| eq(Collections.singletonList(trigger1.getId())), eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao).updateTriggerStatus( |
| eq(Collections.singletonList(trigger2.getId())), eq(Trigger.Status.IGNORED)); |
| // Verify new event report insertion. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao, times(1)) |
| .insertEventReport(reportArg.capture()); |
| List<EventReport> newReportArgs = reportArg.getAllValues(); |
| assertEquals(1, newReportArgs.size()); |
| assertEquals( |
| newReportArgs.get(0).getTriggerData(), |
| triggers.get(0).parseEventTriggers(mFlags).get(0).getTriggerData()); |
| } |
| |
| @Test |
| public void shouldNotCreateEventReportAfterEventReportWindow_prioritisedSource() |
| throws DatastoreException { |
| String eventTriggers = |
| "[{\"trigger_data\": \"5\"," |
| + "\"priority\": \"123\"," |
| + "\"filters\":[{\"key_1\":[\"value_1\"]}]" |
| + "}]"; |
| long eventTime = System.currentTimeMillis(); |
| long triggerTime = eventTime + TimeUnit.DAYS.toMillis(5); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers(eventTriggers) |
| .setTriggerTime(triggerTime) |
| .build(); |
| Source source1 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source1") |
| .setPriority(100L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(eventTime) |
| .setEventReportWindow(triggerTime + 1L) |
| .build(); |
| // Second source has higher priority but the event report window ends before trigger time |
| Source source2 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source2") |
| .setPriority(200L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(eventTime + 1000) |
| .setEventReportWindow(triggerTime - 1) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source1); |
| matchingSourceList.add(source2); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| mHandler.performPendingAttributions(); |
| trigger.setStatus(Trigger.Status.ATTRIBUTED); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| assertEquals(1, matchingSourceList.size()); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldNotAddIfRateLimitExceeded_eventScopeOnly() throws DatastoreException { |
| Trigger trigger = createAPendingTriggerEventScopeOnly(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow(anyInt(), any(), any())) |
| .thenReturn( |
| 5L, |
| (long) Flags.MEASUREMENT_MAX_ATTRIBUTION_PER_RATE_LIMIT_WINDOW); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao).getAttributionsPerRateLimitWindow( |
| Attribution.Scope.EVENT, source, trigger); |
| verify(mMeasurementDao).getAttributionsPerRateLimitWindow( |
| Attribution.Scope.AGGREGATE, source, trigger); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao, never()).insertAggregateReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldNotAddIfRateLimitExceeded_aggregateScopeOnly() |
| throws DatastoreException, JSONException { |
| Trigger trigger = getAggregateTrigger(); |
| Source source = getAggregateSource(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow(anyInt(), any(), any())) |
| .thenReturn( |
| (long) Flags.MEASUREMENT_MAX_AGGREGATE_ATTRIBUTION_PER_RATE_LIMIT_WINDOW, |
| 5L); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao, times(1)).getAttributionsPerRateLimitWindow( |
| Attribution.Scope.EVENT, source, trigger); |
| verify(mMeasurementDao, times(1)).getAttributionsPerRateLimitWindow( |
| Attribution.Scope.AGGREGATE, source, trigger); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao, never()).insertAggregateReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldNotAddIfRateLimitExceeded_aggregateAndEventScope_eventLimited() |
| throws DatastoreException, JSONException { |
| Trigger trigger = getAggregateAndEventTrigger(); |
| Source source = getAggregateSource(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow(anyInt(), any(), any())) |
| .thenReturn( |
| 5L, |
| (long) Flags.MEASUREMENT_MAX_ATTRIBUTION_PER_RATE_LIMIT_WINDOW); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao, times(1)).getAttributionsPerRateLimitWindow( |
| Attribution.Scope.EVENT, source, trigger); |
| verify(mMeasurementDao, times(1)).getAttributionsPerRateLimitWindow( |
| Attribution.Scope.AGGREGATE, source, trigger); |
| verify(mMeasurementDao).updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao, times(1)).insertAggregateReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldNotAddIfRateLimitExceeded_aggregateAndEventScope_aggregateLimited() |
| throws DatastoreException, JSONException { |
| Trigger trigger = getAggregateAndEventTrigger(); |
| Source source = getAggregateSource(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow(anyInt(), any(), any())) |
| .thenReturn( |
| (long) Flags.MEASUREMENT_MAX_AGGREGATE_ATTRIBUTION_PER_RATE_LIMIT_WINDOW, |
| 5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao, times(1)).getAttributionsPerRateLimitWindow( |
| Attribution.Scope.EVENT, source, trigger); |
| verify(mMeasurementDao, times(1)).getAttributionsPerRateLimitWindow( |
| Attribution.Scope.AGGREGATE, source, trigger); |
| verify(mMeasurementDao).updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao, times(1)).insertEventReport(any()); |
| verify(mMeasurementDao, never()).insertAggregateReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldNotAddIfAdTechPrivacyBoundExceeded() throws DatastoreException { |
| Trigger trigger = createAPendingTriggerEventScopeOnly(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.countDistinctReportingOriginsPerPublisherXDestInAttribution( |
| any(), any(), any(), anyLong(), anyLong())) |
| .thenReturn(10); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .countDistinctReportingOriginsPerPublisherXDestInAttribution( |
| any(), any(), any(), anyLong(), anyLong()); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performPendingAttributions_vtcWithConfiguredReportsCount_attributeUptoConfigLimit() |
| throws DatastoreException { |
| // Setup |
| doReturn(true).when(mFlags).getMeasurementEnableVtcConfigurableMaxEventReports(); |
| doReturn(3).when(mFlags).getMeasurementVtcConfigurableMaxEventReportsCount(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| doReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())) |
| .when(mMeasurementDao) |
| .getSourceDestinations(source.getId()); |
| // 2 event reports already present for the source |
| doReturn(Arrays.asList(mock(EventReport.class), mock(EventReport.class))) |
| .when(mMeasurementDao) |
| .getSourceEventReports(source); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| Trigger trigger = createAPendingTriggerEventScopeOnly(); |
| doReturn(trigger).when(mMeasurementDao).getTrigger(trigger.getId()); |
| doReturn(matchingSourceList).when(mMeasurementDao).getMatchingActiveSources(trigger); |
| doReturn(Collections.singletonList(trigger.getId())) |
| .when(mMeasurementDao) |
| .getPendingTriggerIds(); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| |
| // Assertion |
| verify(mMeasurementDao, times(1)).insertEventReport(any()); |
| } |
| |
| @Test |
| public void shouldIgnoreForMaxReportsPerSource() throws DatastoreException { |
| Trigger trigger = createAPendingTriggerEventScopeOnly(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| EventReport eventReport1 = new EventReport.Builder() |
| .setStatus(EventReport.Status.DELIVERED) |
| .build(); |
| EventReport eventReport2 = new EventReport.Builder() |
| .setStatus(EventReport.Status.DELIVERED) |
| .build(); |
| EventReport eventReport3 = new EventReport.Builder().setStatus( |
| EventReport.Status.DELIVERED).build(); |
| List<EventReport> matchingReports = new ArrayList<>(); |
| matchingReports.add(eventReport1); |
| matchingReports.add(eventReport2); |
| matchingReports.add(eventReport3); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mMeasurementDao.getSourceEventReports(source)).thenReturn(matchingReports); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldNotReplaceHighPriorityReports() throws DatastoreException { |
| String eventTriggers = |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"100\",\n" |
| + " \"deduplication_key\": \"2\"\n" |
| + "}" |
| + "]\n"; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setEventTriggers(eventTriggers) |
| .setStatus(Trigger.Status.PENDING) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| EventReport eventReport1 = |
| new EventReport.Builder() |
| .setStatus(EventReport.Status.PENDING) |
| .setTriggerPriority(200L) |
| .build(); |
| EventReport eventReport2 = new EventReport.Builder() |
| .setStatus(EventReport.Status.DELIVERED) |
| .build(); |
| EventReport eventReport3 = new EventReport.Builder() |
| .setStatus(EventReport.Status.DELIVERED) |
| .build(); |
| List<EventReport> matchingReports = new ArrayList<>(); |
| matchingReports.add(eventReport1); |
| matchingReports.add(eventReport2); |
| matchingReports.add(eventReport3); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceEventReports(source)).thenReturn(matchingReports); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldDoSimpleAttribution() throws DatastoreException { |
| Trigger trigger = createAPendingTriggerEventScopeOnly(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("2")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| verify(mMeasurementDao).insertEventReport(any()); |
| |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldDoSimpleAttributionWithNullReports() |
| throws DatastoreException, JSONException { |
| JSONArray triggerDatas = new JSONArray(); |
| JSONObject jsonObject1 = new JSONObject(); |
| jsonObject1.put("key_piece", "0x400"); |
| jsonObject1.put("source_keys", new JSONArray(Arrays.asList("campaignCounts"))); |
| JSONObject jsonObject2 = new JSONObject(); |
| jsonObject2.put("key_piece", "0xA80"); |
| jsonObject2.put("source_keys", new JSONArray(Arrays.asList("geoValue", "noMatch"))); |
| triggerDatas.put(jsonObject1); |
| triggerDatas.put(jsonObject2); |
| |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setAggregateTriggerData(triggerDatas.toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("sourceId1") |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData("{\"product\":[\"1234\",\"2345\"]}") |
| .build(); |
| |
| AggregateReport expectedAggregateReport = |
| new AggregateReport.Builder() |
| .setApiVersion("0.1") |
| .setAttributionDestination(trigger.getAttributionDestination()) |
| .setDebugCleartextPayload( |
| "{\"operation\":\"histogram\"," |
| + "\"data\":[{\"bucket\":\"1369\",\"value\":32768}," |
| + "{\"bucket\":\"2693\",\"value\":1644}]}") |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setPublisher(source.getRegistrant()) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .setSourceRegistrationTime(roundDownToDay(source.getEventTime())) |
| .setAggregateAttributionData( |
| new AggregateAttributionData.Builder() |
| .setContributions( |
| Arrays.asList( |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("1369")) |
| .setValue(32768) |
| .build(), |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("2693")) |
| .setValue(1644) |
| .build())) |
| .build()) |
| .setIsFakeReport(false) |
| .build(); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| when(mFlags.getMeasurementMaxReportingRegisterSourceExpirationInSeconds()) |
| .thenReturn(MEASUREMENT_MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS); |
| when(mFlags.getMeasurementNullAggregateReportEnabled()).thenReturn(true); |
| when(mFlags.getMeasurementNullAggReportRateInclSourceRegistrationTime()).thenReturn(1.0f); |
| |
| mHandler.performPendingAttributions(); |
| |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| // There is a chance to create a null report for each day between 0 and max expiry, |
| // except for the day that the source actually occurred. |
| long invocations = |
| MEASUREMENT_MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS |
| / TimeUnit.DAYS.toSeconds(1); |
| invocations++; // The real report will also be inserted. |
| verify(mMeasurementDao, times((int) invocations)) |
| .insertAggregateReport(aggregateReportCaptor.capture()); |
| List<AggregateReport> reports = aggregateReportCaptor.getAllValues(); |
| |
| List<AggregateReport> fakeReports = |
| reports.stream() |
| .filter(AggregateReport::isFakeReport) |
| .sorted( |
| Comparator.comparing(AggregateReport::getSourceRegistrationTime) |
| .reversed()) |
| .collect(Collectors.toList()); |
| |
| for (int i = 0; i < fakeReports.size(); i++) { |
| // i + 1 because the first attempt at creating a null report is skipped since the |
| // fakeSourceTime would be equal to the actual source registration time. |
| assertNullAggregateReport( |
| fakeReports.get(i), trigger, (i + 1) * TimeUnit.DAYS.toMillis(1)); |
| } |
| |
| AggregateReport trueReport = |
| reports.stream().filter(r -> !r.isFakeReport()).findFirst().get(); |
| assertAggregateReportsEqual(expectedAggregateReport, trueReport); |
| } |
| |
| @Test |
| public void shouldDoSimpleAttributionWithNullReports_triggerOnLastPossibleDay() |
| throws DatastoreException, JSONException { |
| JSONArray triggerDatas = new JSONArray(); |
| JSONObject jsonObject1 = new JSONObject(); |
| jsonObject1.put("key_piece", "0x400"); |
| jsonObject1.put("source_keys", new JSONArray(Arrays.asList("campaignCounts"))); |
| JSONObject jsonObject2 = new JSONObject(); |
| jsonObject2.put("key_piece", "0xA80"); |
| jsonObject2.put("source_keys", new JSONArray(Arrays.asList("geoValue", "noMatch"))); |
| triggerDatas.put(jsonObject1); |
| triggerDatas.put(jsonObject2); |
| |
| // Add one day because attribution is eligible from 0 to max days, inclusive. |
| long maxDays = |
| (MEASUREMENT_MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS |
| + TimeUnit.DAYS.toSeconds(1)) |
| * TimeUnit.SECONDS.toMillis(1); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(roundDownToDay(SOURCE_TIME) + maxDays - 1) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setAggregateTriggerData(triggerDatas.toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("sourceId1") |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(roundDownToDay(SOURCE_TIME) + maxDays) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData("{\"product\":[\"1234\",\"2345\"]}") |
| .build(); |
| |
| AggregateReport expectedAggregateReport = |
| new AggregateReport.Builder() |
| .setApiVersion("0.1") |
| .setAttributionDestination(trigger.getAttributionDestination()) |
| .setDebugCleartextPayload( |
| "{\"operation\":\"histogram\"," |
| + "\"data\":[{\"bucket\":\"1369\",\"value\":32768}," |
| + "{\"bucket\":\"2693\",\"value\":1644}]}") |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setPublisher(source.getRegistrant()) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .setAggregateAttributionData( |
| new AggregateAttributionData.Builder() |
| .setContributions( |
| Arrays.asList( |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("1369")) |
| .setValue(32768) |
| .build(), |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("2693")) |
| .setValue(1644) |
| .build())) |
| .build()) |
| .setIsFakeReport(false) |
| .build(); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow(any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| when(mFlags.getMeasurementMaxReportingRegisterSourceExpirationInSeconds()) |
| .thenReturn(MEASUREMENT_MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS); |
| when(mFlags.getMeasurementNullAggregateReportEnabled()).thenReturn(true); |
| when(mFlags.getMeasurementNullAggReportRateInclSourceRegistrationTime()).thenReturn(1.0f); |
| |
| mHandler.performPendingAttributions(); |
| |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| // There is a chance to create a null report for each day between 0 and max expiry |
| long invocations = |
| MEASUREMENT_MAX_REPORTING_REGISTER_SOURCE_EXPIRATION_IN_SECONDS |
| / TimeUnit.DAYS.toSeconds(1); |
| invocations++; // The real report will also be inserted. |
| verify(mMeasurementDao, times((int) invocations)) |
| .insertAggregateReport(aggregateReportCaptor.capture()); |
| List<AggregateReport> reports = aggregateReportCaptor.getAllValues(); |
| |
| List<AggregateReport> fakeReports = |
| reports.stream() |
| .filter(AggregateReport::isFakeReport) |
| .sorted( |
| Comparator.comparing(AggregateReport::getSourceRegistrationTime) |
| .reversed()) |
| .collect(Collectors.toList()); |
| |
| for (int i = 0; i < fakeReports.size(); i++) { |
| assertNullAggregateReport(fakeReports.get(i), trigger, i * TimeUnit.DAYS.toMillis(1)); |
| } |
| |
| AggregateReport trueReport = |
| reports.stream().filter(r -> !r.isFakeReport()).findFirst().get(); |
| assertAggregateReportsEqual(expectedAggregateReport, trueReport); |
| } |
| |
| @Test |
| public void shouldDoSimpleAttribution_dedupAlignFlagOff() throws DatastoreException { |
| Trigger trigger = createAPendingTriggerEventScopeOnly(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mFlags.getMeasurementEnableAraDeduplicationAlignmentV1()).thenReturn(false); |
| |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); |
| verify(mMeasurementDao).updateSourceEventReportDedupKeys(sourceArg.capture()); |
| assertEquals( |
| sourceArg.getValue().getEventReportDedupKeys(), |
| Collections.singletonList(new UnsignedLong(2L))); |
| verify(mMeasurementDao).insertEventReport(any()); |
| |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldIgnoreLowPrioritySourceWhileAttribution() throws DatastoreException { |
| String eventTriggers = |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"key_1\": [\"value_1\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n"; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers(eventTriggers) |
| .setTriggerTime(3) |
| .build(); |
| Source source1 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source1") |
| .setPriority(100L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(1L) |
| .setExpiryTime(30) |
| .build(); |
| Source source2 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source2") |
| .setPriority(200L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(2L) |
| .setExpiryTime(30) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source1); |
| matchingSourceList.add(source2); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source1.getId())) |
| .thenReturn(Pair.create( |
| source1.getAppDestinations(), |
| source1.getWebDestinations())); |
| when(mMeasurementDao.getSourceDestinations(source2.getId())) |
| .thenReturn(Pair.create( |
| source2.getAppDestinations(), |
| source2.getWebDestinations())); |
| |
| |
| mHandler.performPendingAttributions(); |
| trigger.setStatus(Trigger.Status.ATTRIBUTED); |
| verify(mMeasurementDao) |
| .updateSourceStatus(eq(List.of(source1.getId())), eq(Source.Status.IGNORED)); |
| assertEquals(1, matchingSourceList.size()); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("2")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source2.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void |
| performAttributions_sourceDeactivationAfterFilteringFlagOn_ignoresWithoutAttribution() |
| throws DatastoreException { |
| String eventTriggers = |
| "[{" |
| + " \"trigger_data\": \"5\"," |
| + " \"priority\": \"123\"," |
| + " \"deduplication_key\": \"2\"," |
| + " \"filters\": [{" |
| + " \"key_1\": [\"value_1\"] " |
| + " }]" |
| + "}]"; |
| // Missing top level filters match anything. Competing sources are ignored directly after. |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers(eventTriggers) |
| .setTriggerTime(3) |
| .build(); |
| Source source1 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source1") |
| .setPriority(100L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(1L) |
| .setExpiryTime(30) |
| // Filters match the trigger's event triggers but the Source is not matched |
| // since it has lower priority. |
| .setFilterData("{\"key_1\":[\"1234\",\"value_1\"]}") |
| .build(); |
| Source source2 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source2") |
| .setPriority(200L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(2L) |
| .setExpiryTime(30) |
| // Filters do not match the trigger's event triggers so a report is not |
| // generated. |
| .setFilterData("{\"key_1\":[\"no_match\"]}") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source1); |
| matchingSourceList.add(source2); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source1.getId())) |
| .thenReturn(Pair.create( |
| source1.getAppDestinations(), |
| source1.getWebDestinations())); |
| when(mMeasurementDao.getSourceDestinations(source2.getId())) |
| .thenReturn(Pair.create( |
| source2.getAppDestinations(), |
| source2.getWebDestinations())); |
| |
| when(mFlags.getMeasurementEnableSourceDeactivationAfterFiltering()).thenReturn(true); |
| |
| mHandler.performPendingAttributions(); |
| trigger.setStatus(Trigger.Status.ATTRIBUTED); |
| verify(mMeasurementDao) |
| .updateSourceStatus(eq(List.of(source1.getId())), eq(Source.Status.IGNORED)); |
| assertEquals(1, matchingSourceList.size()); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).updateSourceAttributedTriggers(anyString(), any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void |
| performAttributions_sourceDeactivationAfterFilteringFlagOff_doesNotIgnoreWithoutAttr() |
| throws DatastoreException { |
| String eventTriggers = |
| "[{" |
| + " \"trigger_data\": \"5\"," |
| + " \"priority\": \"123\"," |
| + " \"deduplication_key\": \"2\"," |
| + " \"filters\": [{" |
| + " \"key_1\": [\"value_1\"] " |
| + " }]" |
| + "}]"; |
| // Missing top level filters match anything. |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers(eventTriggers) |
| .setTriggerTime(3) |
| .build(); |
| Source source1 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source1") |
| .setPriority(100L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(1L) |
| .setExpiryTime(30) |
| // Filters match but the Source is not matched since it has lower priority. |
| .setFilterData("{\"key_1\":[\"1234\",\"value_1\"]}") |
| .build(); |
| Source source2 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source2") |
| .setPriority(200L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(2L) |
| .setExpiryTime(30) |
| // Filters do not match, attribution flow returns and does not ignore |
| // competing sources. |
| .setFilterData("{\"key_1\":[\"1234\",\"no_match\"]}") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source1); |
| matchingSourceList.add(source2); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source1.getId())) |
| .thenReturn(Pair.create( |
| source1.getAppDestinations(), |
| source1.getWebDestinations())); |
| when(mMeasurementDao.getSourceDestinations(source2.getId())) |
| .thenReturn(Pair.create( |
| source2.getAppDestinations(), |
| source2.getWebDestinations())); |
| |
| when(mFlags.getMeasurementEnableSourceDeactivationAfterFiltering()).thenReturn(false); |
| |
| mHandler.performPendingAttributions(); |
| trigger.setStatus(Trigger.Status.ATTRIBUTED); |
| // Attribution did not occur and competing sources are not ignored. |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| assertEquals(1, matchingSourceList.size()); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).updateSourceAttributedTriggers(anyString(), any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldReplaceLowPriorityReportWhileAttribution() throws DatastoreException { |
| String eventTriggers = |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"200\",\n" |
| + " \"deduplication_key\": \"2\"\n" |
| + "}" |
| + "]\n"; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setEventTriggers(eventTriggers) |
| .setStatus(Trigger.Status.PENDING) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventReportDedupKeys(new ArrayList<>()) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAppDestinations(List.of(APP_DESTINATION)) |
| .setPublisherType(EventSurfaceType.APP) |
| .setPublisher(PUBLISHER) |
| .build(); |
| doReturn(5L) |
| .when(mEventReportWindowCalcDelegate) |
| .getReportingTime(any(Source.class), anyLong(), anyInt()); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| EventReport eventReport1 = |
| new EventReport.Builder() |
| .setStatus(EventReport.Status.PENDING) |
| .setTriggerPriority(100L) |
| .setReportTime(5L) |
| .setAttributionDestinations(List.of(APP_DESTINATION)) |
| .build(); |
| EventReport eventReport2 = |
| new EventReport.Builder() |
| .setStatus(EventReport.Status.DELIVERED) |
| .setReportTime(5L) |
| .setAttributionDestinations(source.getAppDestinations()) |
| .build(); |
| EventReport eventReport3 = |
| new EventReport.Builder() |
| .setStatus(EventReport.Status.DELIVERED) |
| .setReportTime(5L) |
| .setAttributionDestinations(source.getAppDestinations()) |
| .build(); |
| List<EventReport> matchingReports = new ArrayList<>(); |
| matchingReports.add(eventReport1); |
| matchingReports.add(eventReport2); |
| matchingReports.add(eventReport3); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceEventReports(source)).thenReturn(matchingReports); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| List.of(APP_DESTINATION), |
| List.of())); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao).deleteEventReport(eventReport1); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldRollbackOnFailure() throws DatastoreException { |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers(EVENT_TRIGGERS) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(anyString())).thenReturn(trigger); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| // Failure |
| doThrow(new DatastoreException("Simulating failure")) |
| .when(mMeasurementDao) |
| .insertEventReport(any(EventReport.class)); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao).getTrigger(anyString()); |
| verify(mMeasurementDao).getMatchingActiveSources(any()); |
| verify(mMeasurementDao, times(2)).getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any()); |
| verify(mMeasurementDao, times(1)).insertEventReport(any()); |
| verify(mMeasurementDao, never()).updateTriggerStatus(any(), anyInt()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction).rollback(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldRollbackOnFailure_dedupAlignFlagOff() throws DatastoreException { |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers(EVENT_TRIGGERS) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(anyString())).thenReturn(trigger); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| // Failure |
| when(mFlags.getMeasurementEnableAraDeduplicationAlignmentV1()).thenReturn(false); |
| doThrow(new DatastoreException("Simulating failure")) |
| .when(mMeasurementDao) |
| .updateSourceEventReportDedupKeys(any()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao).getTrigger(anyString()); |
| verify(mMeasurementDao).getMatchingActiveSources(any()); |
| verify(mMeasurementDao, times(2)).getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any()); |
| verify(mMeasurementDao, never()).updateTriggerStatus(any(), anyInt()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction).rollback(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldPerformMultipleAttributions() throws DatastoreException, JSONException { |
| Trigger trigger1 = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .build(); |
| Trigger trigger2 = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId2") |
| .setTriggerTime(TRIGGER_TIME + 1000L) |
| .setStatus(Trigger.Status.PENDING) |
| .setRegistrationOrigin(WebUtil.validUri("https://subdomain2.example.test")) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"2\"\n" |
| + "}" |
| + "]\n") |
| .build(); |
| List<Trigger> triggers = new ArrayList<>(); |
| triggers.add(trigger1); |
| triggers.add(trigger2); |
| List<Source> matchingSourceList1 = new ArrayList<>(); |
| Source source1 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| matchingSourceList1.add(source1); |
| List<Source> matchingSourceList2 = new ArrayList<>(); |
| Source source2 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME + 500L) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setRegistrationOrigin(WebUtil.validUri("https://subdomain2.example.test")) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .build(); |
| matchingSourceList2.add(source2); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Arrays.asList(trigger1.getId(), trigger2.getId())); |
| when(mMeasurementDao.getTrigger(trigger1.getId())).thenReturn(trigger1); |
| when(mMeasurementDao.getTrigger(trigger2.getId())).thenReturn(trigger2); |
| when(mMeasurementDao.getMatchingActiveSources(trigger1)).thenReturn(matchingSourceList1); |
| when(mMeasurementDao.getMatchingActiveSources(trigger2)).thenReturn(matchingSourceList2); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source1.getId())) |
| .thenReturn(Pair.create( |
| source1.getAppDestinations(), |
| source1.getWebDestinations())); |
| when(mMeasurementDao.getSourceDestinations(source2.getId())) |
| .thenReturn(Pair.create( |
| source2.getAppDestinations(), |
| source2.getWebDestinations())); |
| |
| assertTrue(mHandler.performPendingAttributions()); |
| // Verify trigger status updates. |
| verify(mMeasurementDao, times(2)).updateTriggerStatus(any(), eq(Trigger.Status.ATTRIBUTED)); |
| // Verify source dedup key updates. |
| String expectedAttributionStatus1 = getAttributionStatus( |
| List.of(trigger1.getId()), List.of("5"), List.of("1")); |
| String expectedAttributionStatus2 = getAttributionStatus( |
| List.of(trigger2.getId()), List.of("5"), List.of("2")); |
| ArgumentCaptor<String> sourceIdArg = ArgumentCaptor.forClass(String.class); |
| ArgumentCaptor<String> attributionStatusArg = ArgumentCaptor.forClass(String.class); |
| verify(mMeasurementDao, times(2)).updateSourceAttributedTriggers( |
| sourceIdArg.capture(), attributionStatusArg.capture()); |
| List<String> sourceIds = sourceIdArg.getAllValues(); |
| List<String> attributionStatuses = attributionStatusArg.getAllValues(); |
| assertEquals(source1.getId(), sourceIds.get(0)); |
| assertEquals(expectedAttributionStatus1, attributionStatuses.get(0)); |
| assertEquals(source2.getId(), sourceIds.get(1)); |
| assertEquals(expectedAttributionStatus2, attributionStatuses.get(1)); |
| // Verify new event report insertion. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao, times(2)).insertEventReport(reportArg.capture()); |
| List<EventReport> newReportArgs = reportArg.getAllValues(); |
| boolean flexEventReportFlag = true; |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| for (int i = 0; i < newReportArgs.size(); i++) { |
| assertEquals( |
| newReportArgs.get(i).getTriggerDedupKey(), |
| triggers.get(i).parseEventTriggers(mFlags).get(0).getDedupKey()); |
| assertEquals( |
| newReportArgs.get(i).getRegistrationOrigin(), |
| triggers.get(i).getRegistrationOrigin()); |
| } |
| } |
| |
| @Test |
| public void shouldAttributedToInstallAttributedSource() throws DatastoreException { |
| long eventTime = System.currentTimeMillis(); |
| long triggerTime = eventTime + TimeUnit.DAYS.toMillis(5); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("trigger1") |
| .setStatus(Trigger.Status.PENDING) |
| .setTriggerTime(triggerTime) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"2\"\n" |
| + "}" |
| + "]\n") |
| .build(); |
| // Lower priority and older priority source. |
| Source source1 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source1") |
| .setEventId(new UnsignedLong(1L)) |
| .setPriority(100L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setInstallAttributed(true) |
| .setInstallCooldownWindow(TimeUnit.DAYS.toMillis(10)) |
| .setEventTime(eventTime - TimeUnit.DAYS.toMillis(2)) |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| Source source2 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source2") |
| .setEventId(new UnsignedLong(2L)) |
| .setPriority(200L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(eventTime) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source2); |
| matchingSourceList.add(source1); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source1.getId())) |
| .thenReturn(Pair.create( |
| source1.getAppDestinations(), |
| source1.getWebDestinations())); |
| when(mMeasurementDao.getSourceDestinations(source2.getId())) |
| .thenReturn(Pair.create( |
| source2.getAppDestinations(), |
| source2.getWebDestinations())); |
| mHandler.performPendingAttributions(); |
| trigger.setStatus(Trigger.Status.ATTRIBUTED); |
| verify(mMeasurementDao) |
| .updateSourceStatus(eq(List.of(source2.getId())), eq(Source.Status.IGNORED)); |
| assertEquals(1, matchingSourceList.size()); |
| assertEquals(source2.getEventId(), matchingSourceList.get(0).getEventId()); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("2")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source1.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| } |
| |
| @Test |
| public void shouldNotAttributeToOldInstallAttributedSource() throws DatastoreException { |
| // Setup |
| long eventTime = System.currentTimeMillis(); |
| long triggerTime = eventTime + TimeUnit.DAYS.toMillis(10); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("trigger1") |
| .setStatus(Trigger.Status.PENDING) |
| .setTriggerTime(triggerTime) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"2\"\n" |
| + "}" |
| + "]\n") |
| .build(); |
| // Lower Priority. Install cooldown Window passed. |
| Source source1 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source1") |
| .setEventId(new UnsignedLong(1L)) |
| .setPriority(100L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setInstallAttributed(true) |
| .setInstallCooldownWindow(TimeUnit.DAYS.toMillis(3)) |
| .setEventTime(eventTime - TimeUnit.DAYS.toMillis(2)) |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| Source source2 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("source2") |
| .setEventId(new UnsignedLong(2L)) |
| .setPriority(200L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(eventTime) |
| .setEventReportWindow(triggerTime + 1L) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source2); |
| matchingSourceList.add(source1); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source1.getId())) |
| .thenReturn(Pair.create( |
| source1.getAppDestinations(), |
| source1.getWebDestinations())); |
| when(mMeasurementDao.getSourceDestinations(source2.getId())) |
| .thenReturn(Pair.create( |
| source2.getAppDestinations(), |
| source2.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| trigger.setStatus(Trigger.Status.ATTRIBUTED); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateSourceStatus(eq(List.of(source1.getId())), eq(Source.Status.IGNORED)); |
| assertEquals(1, matchingSourceList.size()); |
| assertEquals(source1.getEventId(), matchingSourceList.get(0).getEventId()); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| ArgumentCaptor<String> sourceIdArg = ArgumentCaptor.forClass(String.class); |
| ArgumentCaptor<String> attributionStatusArg = ArgumentCaptor.forClass(String.class); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("2")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| sourceIdArg.capture(), attributionStatusArg.capture()); |
| assertEquals(source2.getId(), sourceIdArg.getValue()); |
| assertEquals( |
| expectedAttributionStatus, |
| attributionStatusArg.getValue()); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| } |
| |
| @Test |
| public void shouldNotGenerateReportForAttributionModeFalsely() throws DatastoreException { |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.FALSELY) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).updateSourceEventReportDedupKeys(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void shouldNotGenerateReportForAttributionModeNever() throws DatastoreException { |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.NEVER) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).updateSourceEventReportDedupKeys(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performPendingAttributions_GeneratesEventReport_WithReportingOriginOfTrigger() |
| throws DatastoreException { |
| Trigger trigger1 = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setRegistrationOrigin(WebUtil.validUri("https://trigger.example.test")) |
| .build(); |
| List<Source> matchingSourceList1 = new ArrayList<>(); |
| Source source1 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setRegistrationOrigin(WebUtil.validUri("https://source.example.test")) |
| .build(); |
| matchingSourceList1.add(source1); |
| when(mMeasurementDao.getPendingTriggerIds()).thenReturn(Arrays.asList(trigger1.getId())); |
| when(mMeasurementDao.getTrigger(trigger1.getId())).thenReturn(trigger1); |
| when(mMeasurementDao.getMatchingActiveSources(trigger1)).thenReturn(matchingSourceList1); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source1.getId())) |
| .thenReturn( |
| Pair.create(source1.getAppDestinations(), source1.getWebDestinations())); |
| |
| assertTrue(mHandler.performPendingAttributions()); |
| // Verify trigger status updates. |
| verify(mMeasurementDao).updateTriggerStatus(any(), eq(Trigger.Status.ATTRIBUTED)); |
| // Verify new event report insertion. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals( |
| WebUtil.validUri("https://trigger.example.test"), |
| eventReport.getRegistrationOrigin()); |
| } |
| |
| @Test |
| public void performPendingAttributions_GeneratesEventReport_WithCoarseDestinations() |
| throws DatastoreException { |
| Trigger trigger1 = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setRegistrationOrigin(WebUtil.validUri("https://trigger.example.test")) |
| .build(); |
| List<Source> matchingSourceList1 = new ArrayList<>(); |
| Source source1 = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setRegistrationOrigin(WebUtil.validUri("https://source.example.test")) |
| .setAppDestinations(Collections.singletonList(APP_DESTINATION)) |
| .setWebDestinations(Collections.singletonList(WEB_DESTINATION)) |
| .setCoarseEventReportDestinations(true) |
| .build(); |
| matchingSourceList1.add(source1); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger1.getId())); |
| when(mMeasurementDao.getTrigger(trigger1.getId())).thenReturn(trigger1); |
| when(mMeasurementDao.getMatchingActiveSources(trigger1)).thenReturn(matchingSourceList1); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source1.getId())) |
| .thenReturn( |
| Pair.create(source1.getAppDestinations(), source1.getWebDestinations())); |
| when(mFlags.getMeasurementEnableCoarseEventReportDestinations()).thenReturn(true); |
| |
| assertTrue(mHandler.performPendingAttributions()); |
| // Verify trigger status updates. |
| verify(mMeasurementDao).updateTriggerStatus(any(), eq(Trigger.Status.ATTRIBUTED)); |
| // Verify new event report insertion. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals( |
| WebUtil.validUri("https://trigger.example.test"), |
| eventReport.getRegistrationOrigin()); |
| List<Uri> reportDestinations = eventReport.getAttributionDestinations(); |
| assertEquals(2, reportDestinations.size()); |
| assertEquals(APP_DESTINATION, reportDestinations.get(0)); |
| assertEquals(WEB_DESTINATION, reportDestinations.get(1)); |
| } |
| |
| @Test |
| public void shouldObserveFlagOverriddenAggregateReportDelay() |
| throws DatastoreException, JSONException { |
| Trigger trigger = getAggregateTrigger(); |
| Source source = getAggregateSource(); |
| |
| long reportMinDelay = TimeUnit.MINUTES.toMillis(61); |
| long reportDelaySpan = TimeUnit.MINUTES.toMillis(10); |
| when(mFlags.getMeasurementEnableConfigurableAggregateReportDelay()).thenReturn(true); |
| when(mFlags.getMeasurementAggregateReportDelayConfig()).thenReturn( |
| String.valueOf(reportMinDelay) + "," + String.valueOf(reportDelaySpan)); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| mHandler.performPendingAttributions(); |
| |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(aggregateReportCaptor.capture()); |
| |
| // Assert expected aggregate report time range |
| long lowerBound = TRIGGER_TIME + reportMinDelay; |
| // Add slightly more delay to upper bound to account for execution. |
| long upperBound = TRIGGER_TIME + reportMinDelay + reportDelaySpan + 1000L; |
| AggregateReport capturedReport = aggregateReportCaptor.getValue(); |
| assertTrue(capturedReport.getScheduledReportTime() > lowerBound |
| && capturedReport.getScheduledReportTime() < upperBound); |
| } |
| |
| @Test |
| public void shouldObserveDefaultAggregateReportDelay() |
| throws DatastoreException, JSONException { |
| Trigger trigger = getAggregateTrigger(); |
| Source source = getAggregateSource(); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| mHandler.performPendingAttributions(); |
| |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(aggregateReportCaptor.capture()); |
| |
| // Assert expected aggregate report time range |
| long lowerBound = TRIGGER_TIME + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY; |
| // Add slightly more delay to upper bound to account for execution. |
| long upperBound = TRIGGER_TIME + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY |
| + PrivacyParams.AGGREGATE_REPORT_DELAY_SPAN + 1000L; |
| AggregateReport capturedReport = aggregateReportCaptor.getValue(); |
| assertTrue(capturedReport.getScheduledReportTime() > lowerBound |
| && capturedReport.getScheduledReportTime() < upperBound); |
| } |
| |
| @Test |
| public void shouldObserveDefaultAggregateReportDelayWhenFlagOverrideIsNull() |
| throws DatastoreException, JSONException { |
| Trigger trigger = getAggregateTrigger(); |
| Source source = getAggregateSource(); |
| |
| when(mFlags.getMeasurementEnableConfigurableAggregateReportDelay()).thenReturn(true); |
| when(mFlags.getMeasurementAggregateReportDelayConfig()).thenReturn(null); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| mHandler.performPendingAttributions(); |
| |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(aggregateReportCaptor.capture()); |
| |
| // Assert expected aggregate report time range |
| long lowerBound = TRIGGER_TIME + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY; |
| // Add slightly more delay to upper bound to account for execution. |
| long upperBound = TRIGGER_TIME + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY |
| + PrivacyParams.AGGREGATE_REPORT_DELAY_SPAN + 1000L; |
| AggregateReport capturedReport = aggregateReportCaptor.getValue(); |
| assertTrue(capturedReport.getScheduledReportTime() > lowerBound |
| && capturedReport.getScheduledReportTime() < upperBound); |
| } |
| |
| @Test |
| public void shouldObserveDefaultAggregateReportDelayWhenFlagOverrideSizeIsInvalid() |
| throws DatastoreException, JSONException { |
| Trigger trigger = getAggregateTrigger(); |
| Source source = getAggregateSource(); |
| |
| when(mFlags.getMeasurementEnableConfigurableAggregateReportDelay()).thenReturn(true); |
| when(mFlags.getMeasurementAggregateReportDelayConfig()).thenReturn("12"); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| mHandler.performPendingAttributions(); |
| |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(aggregateReportCaptor.capture()); |
| |
| // Assert expected aggregate report time range |
| long lowerBound = TRIGGER_TIME + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY; |
| // Add slightly more delay to upper bound to account for execution. |
| long upperBound = TRIGGER_TIME + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY |
| + PrivacyParams.AGGREGATE_REPORT_DELAY_SPAN + 1000L; |
| AggregateReport capturedReport = aggregateReportCaptor.getValue(); |
| assertTrue(capturedReport.getScheduledReportTime() > lowerBound |
| && capturedReport.getScheduledReportTime() < upperBound); |
| } |
| |
| @Test |
| public void shouldObserveDefaultAggregateReportDelayWhenFlagOverrideValueIsInvalid() |
| throws DatastoreException, JSONException { |
| Trigger trigger = getAggregateTrigger(); |
| Source source = getAggregateSource(); |
| |
| when(mFlags.getMeasurementEnableConfigurableAggregateReportDelay()).thenReturn(true); |
| when(mFlags.getMeasurementAggregateReportDelayConfig()).thenReturn("1200u0000r,3600000"); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| mHandler.performPendingAttributions(); |
| |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(aggregateReportCaptor.capture()); |
| |
| // Assert expected aggregate report time range |
| long lowerBound = TRIGGER_TIME + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY; |
| // Add slightly more delay to upper bound to account for execution. |
| long upperBound = TRIGGER_TIME + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY |
| + PrivacyParams.AGGREGATE_REPORT_DELAY_SPAN + 1000L; |
| AggregateReport capturedReport = aggregateReportCaptor.getValue(); |
| assertTrue(capturedReport.getScheduledReportTime() > lowerBound |
| && capturedReport.getScheduledReportTime() < upperBound); |
| } |
| |
| @Test |
| public void shouldDoSimpleAttributionGenerateUnencryptedAggregateReport() |
| throws DatastoreException, JSONException { |
| JSONArray triggerDatas = new JSONArray(); |
| JSONObject jsonObject1 = new JSONObject(); |
| jsonObject1.put("key_piece", "0x400"); |
| jsonObject1.put("source_keys", new JSONArray(Arrays.asList("campaignCounts"))); |
| JSONObject jsonObject2 = new JSONObject(); |
| jsonObject2.put("key_piece", "0xA80"); |
| jsonObject2.put("source_keys", new JSONArray(Arrays.asList("geoValue", "noMatch"))); |
| triggerDatas.put(jsonObject1); |
| triggerDatas.put(jsonObject2); |
| |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setAggregateTriggerData(triggerDatas.toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("sourceId1") |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData("{\"product\":[\"1234\",\"2345\"]}") |
| .build(); |
| |
| AggregateReport expectedAggregateReport = |
| new AggregateReport.Builder() |
| .setApiVersion("0.1") |
| .setAttributionDestination(trigger.getAttributionDestination()) |
| .setDebugCleartextPayload( |
| "{\"operation\":\"histogram\"," |
| + "\"data\":[{\"bucket\":\"1369\",\"value\":32768}," |
| + "{\"bucket\":\"2693\",\"value\":1644}]}") |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setPublisher(source.getRegistrant()) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .setAggregateAttributionData( |
| new AggregateAttributionData.Builder() |
| .setContributions( |
| Arrays.asList( |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("1369")) |
| .setValue(32768) |
| .build(), |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("2693")) |
| .setValue(1644) |
| .build())) |
| .build()) |
| .build(); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| mHandler.performPendingAttributions(); |
| ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); |
| verify(mMeasurementDao).updateSourceAggregateContributions(sourceArg.capture()); |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(aggregateReportCaptor.capture()); |
| |
| assertAggregateReportsEqual(expectedAggregateReport, aggregateReportCaptor.getValue()); |
| assertEquals(sourceArg.getValue().getAggregateContributions(), 32768 + 1644); |
| } |
| |
| @Test |
| public void performPendingAttributions_GeneratesAggregateReport_WithReportingOriginOfTrigger() |
| throws JSONException, DatastoreException { |
| JSONArray triggerDatas = new JSONArray(); |
| JSONObject jsonObject1 = new JSONObject(); |
| jsonObject1.put("key_piece", "0x400"); |
| jsonObject1.put("source_keys", new JSONArray(Arrays.asList("campaignCounts"))); |
| JSONObject jsonObject2 = new JSONObject(); |
| jsonObject2.put("key_piece", "0xA80"); |
| jsonObject2.put("source_keys", new JSONArray(Arrays.asList("geoValue", "noMatch"))); |
| triggerDatas.put(jsonObject1); |
| triggerDatas.put(jsonObject2); |
| |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setAggregateTriggerData(triggerDatas.toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .setRegistrationOrigin(WebUtil.validUri("https://trigger.example.test")) |
| .build(); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("sourceId1") |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData("{\"product\":[\"1234\",\"2345\"]}") |
| .setRegistrationOrigin(WebUtil.validUri("https://source.example.test")) |
| .build(); |
| |
| AggregateReport expectedAggregateReport = |
| new AggregateReport.Builder() |
| .setApiVersion("0.1") |
| .setAttributionDestination(trigger.getAttributionDestination()) |
| .setDebugCleartextPayload( |
| "{\"operation\":\"histogram\"," |
| + "\"data\":[{\"bucket\":\"1369\",\"value\":32768}," |
| + "{\"bucket\":\"2693\",\"value\":1644}]}") |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setPublisher(source.getRegistrant()) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(WebUtil.validUri("https://trigger.example.test")) |
| .setAggregateAttributionData( |
| new AggregateAttributionData.Builder() |
| .setContributions( |
| Arrays.asList( |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("1369")) |
| .setValue(32768) |
| .build(), |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("2693")) |
| .setValue(1644) |
| .build())) |
| .build()) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| |
| mHandler.performPendingAttributions(); |
| |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(aggregateReportCaptor.capture()); |
| assertAggregateReportsEqual(expectedAggregateReport, aggregateReportCaptor.getValue()); |
| } |
| |
| @Test |
| public void shouldDoSimpleAttributionGenerateUnencryptedAggregateReportWithDedupKey() |
| throws DatastoreException, JSONException { |
| JSONArray triggerDatas = new JSONArray(); |
| JSONObject jsonObject1 = new JSONObject(); |
| jsonObject1.put("key_piece", "0x400"); |
| jsonObject1.put("source_keys", new JSONArray(Arrays.asList("campaignCounts"))); |
| JSONObject jsonObject2 = new JSONObject(); |
| jsonObject2.put("key_piece", "0xA80"); |
| jsonObject2.put("source_keys", new JSONArray(Arrays.asList("geoValue", "noMatch"))); |
| triggerDatas.put(jsonObject1); |
| triggerDatas.put(jsonObject2); |
| |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setAggregateTriggerData(triggerDatas.toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .setAggregateDeduplicationKeys(AGGREGATE_DEDUPLICATION_KEYS_1) |
| .build(); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setId("sourceId1") |
| .setEventTime(SOURCE_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData("{\"product\":[\"1234\",\"2345\"]}") |
| .build(); |
| AggregateReport expectedAggregateReport = |
| new AggregateReport.Builder() |
| .setApiVersion("0.1") |
| .setAttributionDestination(trigger.getAttributionDestination()) |
| .setDebugCleartextPayload( |
| "{\"operation\":\"histogram\"," |
| + "\"data\":[{\"bucket\":\"1369\",\"value\":32768}," |
| + "{\"bucket\":\"2693\",\"value\":1644}]}") |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setPublisher(source.getRegistrant()) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .setAggregateAttributionData( |
| new AggregateAttributionData.Builder() |
| .setContributions( |
| Arrays.asList( |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("1369")) |
| .setValue(32768) |
| .build(), |
| new AggregateHistogramContribution.Builder() |
| .setKey(new BigInteger("2693")) |
| .setValue(1644) |
| .build())) |
| .build()) |
| .build(); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| mHandler.performPendingAttributions(); |
| ArgumentCaptor<Source> sourceArg = ArgumentCaptor.forClass(Source.class); |
| verify(mMeasurementDao).updateSourceAggregateContributions(sourceArg.capture()); |
| ArgumentCaptor<AggregateReport> aggregateReportCaptor = |
| ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(aggregateReportCaptor.capture()); |
| |
| assertAggregateReportsEqual(expectedAggregateReport, aggregateReportCaptor.getValue()); |
| assertEquals(sourceArg.getValue().getAggregateContributions(), 32768 + 1644); |
| ArgumentCaptor<Source> sourceCaptor = ArgumentCaptor.forClass(Source.class); |
| verify(mMeasurementDao).updateSourceAggregateReportDedupKeys(sourceCaptor.capture()); |
| |
| assertEquals(sourceCaptor.getValue().getAggregateReportDedupKeys().size(), 1); |
| assertEquals( |
| sourceCaptor.getValue().getAggregateReportDedupKeys().get(0), |
| new UnsignedLong(10L)); |
| } |
| |
| @Test |
| public void shouldNotGenerateAggregateReportWhenExceedingAggregateContributionsLimit() |
| throws DatastoreException, JSONException { |
| JSONArray triggerDatas = new JSONArray(); |
| JSONObject jsonObject1 = new JSONObject(); |
| jsonObject1.put("key_piece", "0x400"); |
| jsonObject1.put("source_keys", new JSONArray(Arrays.asList("campaignCounts"))); |
| JSONObject jsonObject2 = new JSONObject(); |
| jsonObject2.put("key_piece", "0xA80"); |
| jsonObject2.put("source_keys", new JSONArray(Arrays.asList("geoValue", "noMatch"))); |
| triggerDatas.put(jsonObject1); |
| triggerDatas.put(jsonObject2); |
| |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setAggregateTriggerData(triggerDatas.toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .setEventTriggers(EVENT_TRIGGERS) |
| .build(); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData("{\"product\":[\"1234\",\"2345\"]}") |
| .setAggregateContributions(65536 - 32768 - 1644 + 1) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| mHandler.performPendingAttributions(); |
| verify(mMeasurementDao, never()).updateSourceAggregateContributions(any()); |
| verify(mMeasurementDao, never()).insertAggregateReport(any()); |
| } |
| |
| @Test |
| public void performAttributions_triggerFilterSet_commonKeysDontIntersect_ignoreTrigger() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"]}, {\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1_x\", \"value_2_x\"],\n" |
| + " \"key_2\": [\"value_1_x\", \"value_2_x\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).updateSourceEventReportDedupKeys(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_triggerFilters_commonKeysDontIntersect_ignoreTrigger() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1_x\", \"value_2_x\"],\n" |
| + " \"key_2\": [\"value_1_x\", \"value_2_x\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).updateSourceEventReportDedupKeys(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_triggerNotFilterSet_commonKeysIntersect_ignoreTrigger() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setNotFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"]}, {\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2_x\"],\n" |
| + " \"key_2\": [\"value_1_x\", \"value_2\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).updateSourceEventReportDedupKeys(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_triggerNotFilters_commonKeysIntersect_ignoreTrigger() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setNotFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2_x\"],\n" |
| + " \"key_2\": [\"value_1_x\", \"value_2\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).updateSourceEventReportDedupKeys(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_triggerNotFilterSet_commonKeysDontIntersect_attributeTrigger() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setNotFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11_x\", \"value_12\"]}, {\n" |
| + " \"key_2\": [\"value_21\", \"value_22_x\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" |
| + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("1")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_triggerNotFiltersWithCommonKeysDontIntersect_attributeTrigger() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setNotFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11_x\", \"value_12\"],\n" |
| + " \"key_2\": [\"value_21\", \"value_22_x\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" |
| + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("1")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_topLevelFilterSetMatch_attributeTrigger() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11_x\", \"value_12\"]}, {\n" |
| + " \"key_2\": [\"value_21\", \"value_22\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" |
| + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("1")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_triggerSourceFiltersWithCommonKeysIntersect_attributeTrigger() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11\", \"value_12\"],\n" |
| + " \"key_2\": [\"value_21\", \"value_22\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" |
| + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("1")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_commonKeysIntersect_attributeTrigger_debugApi() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11\", \"value_12\"],\n" |
| + " \"key_2\": [\"value_21\", \"value_22\"]\n" |
| + "}]\n") |
| .setDebugKey(TRIGGER_DEBUG_KEY) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" |
| + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" |
| + "}\n") |
| .setDebugKey(SOURCE_DEBUG_KEY) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("1")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_commonKeysIntersect_attributeTrigger_debugApi_sourceKey() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11\", \"value_12\"],\n" |
| + " \"key_2\": [\"value_21\", \"value_22\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" |
| + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" |
| + "}\n") |
| .setDebugKey(SOURCE_DEBUG_KEY) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("1")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_commonKeysIntersect_attributeTrigger_debugApi_triggerKey() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11\", \"value_12\"],\n" |
| + " \"key_2\": [\"value_21\", \"value_22\"]\n" |
| + "}]\n") |
| .setDebugKey(TRIGGER_DEBUG_KEY) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_11\", \"value_12_x\"],\n" |
| + " \"key_2\": [\"value_21_x\", \"value_22\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("1")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_triggerSourceFiltersWithNoCommonKeys_attributeTrigger() |
| throws DatastoreException { |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11\", \"value_12\"],\n" |
| + " \"key_2\": [\"value_21\", \"value_22\"]\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1x\": [\"value_11_x\", \"value_12_x\"],\n" |
| + " \"key_2x\": [\"value_21_x\", \"value_22_x\"]\n" |
| + "}\n") |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("5"), List.of("1")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_eventLevelFilters_filterSet_attributeFirstMatchingTrigger() |
| throws DatastoreException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"2\",\n" |
| + " \"priority\": \"2\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"key_1\": [\"unmatched\"] \n" |
| + " }]\n" |
| + "}," |
| + "{\n" |
| + " \"trigger_data\": \"3\",\n" |
| + " \"priority\": \"3\",\n" |
| + " \"deduplication_key\": \"3\",\n" |
| + " \"filters\": [{\n" |
| + " \"ignored\": [\"ignored\"]}, {\n" |
| + " \"key_1\": [\"unmatched\"]}, {\n" |
| + " \"key_1\": [\"matched\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .setTriggerTime(triggerTime) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"matched\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setId("sourceId") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| EventReport expectedEventReport = |
| new EventReport.Builder() |
| .setTriggerPriority(3L) |
| .setTriggerDedupKey(new UnsignedLong(3L)) |
| .setTriggerData(new UnsignedLong(1L)) |
| .setTriggerTime(triggerTime) |
| .setSourceEventId(source.getEventId()) |
| .setStatus(EventReport.Status.PENDING) |
| .setAttributionDestinations(source.getAppDestinations()) |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setReportTime( |
| mEventReportWindowCalcDelegate.getReportingTime( |
| source, |
| trigger.getTriggerTime(), |
| trigger.getDestinationType())) |
| .setSourceType(source.getSourceType()) |
| .setRandomizedTriggerRate( |
| mSourceNoiseHandler.getRandomAttributionProbability(source)) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("3"), List.of("3")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| verify(mMeasurementDao).insertEventReport(eq(expectedEventReport)); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_eventLevelFilters_attributeFirstMatchingTrigger() |
| throws DatastoreException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"2\",\n" |
| + " \"priority\": \"2\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"key_1\": [\"value_1_x\"] \n" |
| + " }]\n" |
| + "}," |
| + "{\n" |
| + " \"trigger_data\": \"3\",\n" |
| + " \"priority\": \"3\",\n" |
| + " \"deduplication_key\": \"3\",\n" |
| + " \"filters\": [{\n" |
| + " \"key_1\": [\"value_1\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .setTriggerTime(triggerTime) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setId("sourceId") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| EventReport expectedEventReport = |
| new EventReport.Builder() |
| .setTriggerPriority(3L) |
| .setTriggerDedupKey(new UnsignedLong(3L)) |
| .setTriggerData(new UnsignedLong(1L)) |
| .setTriggerTime(triggerTime) |
| .setSourceEventId(source.getEventId()) |
| .setStatus(EventReport.Status.PENDING) |
| .setAttributionDestinations(source.getAppDestinations()) |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setReportTime( |
| mEventReportWindowCalcDelegate.getReportingTime( |
| source, |
| trigger.getTriggerTime(), |
| trigger.getDestinationType())) |
| .setSourceType(source.getSourceType()) |
| .setRandomizedTriggerRate( |
| mSourceNoiseHandler.getRandomAttributionProbability(source)) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("3"), List.of("3")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| verify(mMeasurementDao).insertEventReport(eq(expectedEventReport)); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_filterSet_eventLevelNotFilters_attributeFirstMatchingTrigger() |
| throws DatastoreException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"2\",\n" |
| + " \"priority\": \"2\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"not_filters\": [{\n" |
| + " \"key_1\": [\"value_1\"] \n" |
| + " }]\n" |
| + "}," |
| + "{\n" |
| + " \"trigger_data\": \"3\",\n" |
| + " \"priority\": \"3\",\n" |
| + " \"deduplication_key\": \"3\",\n" |
| + " \"not_filters\": [{\n" |
| + " \"key_1\": [\"value_1\"]}, {\n" |
| + " \"key_2\": [\"value_2\"]}, {\n" |
| + " \"key_1\": [\"matches_when_negated\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .setTriggerTime(triggerTime) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setId("sourceId") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| EventReport expectedEventReport = |
| new EventReport.Builder() |
| .setTriggerPriority(3L) |
| .setTriggerDedupKey(new UnsignedLong(3L)) |
| .setTriggerData(new UnsignedLong(1L)) |
| .setTriggerTime(triggerTime) |
| .setSourceEventId(source.getEventId()) |
| .setStatus(EventReport.Status.PENDING) |
| .setAttributionDestinations(source.getAppDestinations()) |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setReportTime( |
| mEventReportWindowCalcDelegate.getReportingTime( |
| source, |
| trigger.getTriggerTime(), |
| trigger.getDestinationType())) |
| .setSourceType(source.getSourceType()) |
| .setRandomizedTriggerRate( |
| mSourceNoiseHandler.getRandomAttributionProbability(source)) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("3"), List.of("3")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| verify(mMeasurementDao).insertEventReport(eq(expectedEventReport)); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_eventLevelNotFilters_attributeFirstMatchingTrigger() |
| throws DatastoreException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"2\",\n" |
| + " \"priority\": \"2\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"not_filters\": [{\n" |
| + " \"key_1\": [\"value_1\"] \n" |
| + " }]\n" |
| + "}," |
| + "{\n" |
| + " \"trigger_data\": \"3\",\n" |
| + " \"priority\": \"3\",\n" |
| + " \"deduplication_key\": \"3\",\n" |
| + " \"not_filters\": [{\n" |
| + " \"key_1\": [\"value_1_x\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .setTriggerTime(triggerTime) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setId("sourceId") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| EventReport expectedEventReport = |
| new EventReport.Builder() |
| .setTriggerPriority(3L) |
| .setTriggerDedupKey(new UnsignedLong(3L)) |
| .setTriggerData(new UnsignedLong(1L)) |
| .setTriggerTime(triggerTime) |
| .setSourceEventId(source.getEventId()) |
| .setStatus(EventReport.Status.PENDING) |
| .setAttributionDestinations(source.getAppDestinations()) |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setReportTime( |
| mEventReportWindowCalcDelegate.getReportingTime( |
| source, |
| trigger.getTriggerTime(), |
| trigger.getDestinationType())) |
| .setSourceType(source.getSourceType()) |
| .setRandomizedTriggerRate( |
| mSourceNoiseHandler.getRandomAttributionProbability(source)) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("3"), List.of("3")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| verify(mMeasurementDao).insertEventReport(eq(expectedEventReport)); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_eventLevelFiltersWithSourceType_attributeFirstMatchingTrigger() |
| throws DatastoreException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"2\",\n" |
| + " \"priority\": \"2\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"source_type\": [\"event\"], \n" |
| + " \"dummy_key\": [\"dummy_value\"] \n" |
| + " }]\n" |
| + "}," |
| + "{\n" |
| + " \"trigger_data\": \"3\",\n" |
| + " \"priority\": \"3\",\n" |
| + " \"deduplication_key\": \"3\",\n" |
| + " \"filters\": [{\n" |
| + " \"source_type\": [\"navigation\"], \n" |
| + " \"dummy_key\": [\"dummy_value\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .setTriggerTime(triggerTime) |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setId("sourceId") |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| EventReport expectedEventReport = |
| new EventReport.Builder() |
| .setTriggerPriority(3L) |
| .setTriggerDedupKey(new UnsignedLong(3L)) |
| .setTriggerData(new UnsignedLong(3L)) |
| .setTriggerTime(234324L) |
| .setSourceEventId(source.getEventId()) |
| .setStatus(EventReport.Status.PENDING) |
| .setAttributionDestinations(source.getAppDestinations()) |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setReportTime( |
| mEventReportWindowCalcDelegate.getReportingTime( |
| source, |
| trigger.getTriggerTime(), |
| trigger.getDestinationType())) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRandomizedTriggerRate( |
| mSourceNoiseHandler.getRandomAttributionProbability(source)) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = getAttributionStatus( |
| List.of(trigger.getId()), List.of("3"), List.of("3")); |
| verify(mMeasurementDao).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(expectedAttributionStatus)); |
| verify(mMeasurementDao).insertEventReport(eq(expectedEventReport)); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_filterSet_eventLevelFiltersFailToMatch_aggregateReportOnly() |
| throws DatastoreException, JSONException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"2\",\n" |
| + " \"priority\": \"2\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"product\": [\"value_11\"]}, {\n" |
| + " \"key_1\": [\"value_11\"] \n" |
| + " }]\n" |
| + "}," |
| + "{\n" |
| + " \"trigger_data\": \"3\",\n" |
| + " \"priority\": \"3\",\n" |
| + " \"deduplication_key\": \"3\",\n" |
| + " \"filters\": [{\n" |
| + " \"product\": [\"value_21\"]}, {\n" |
| + " \"key_1\": [\"value_21\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\"product\":[\"1234\", \"2345\"]," |
| + "\"key_1\": [\"value_1_y\", \"value_2_y\"]}") |
| .setId("sourceId") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| // Verify aggregate report registration origin. |
| ArgumentCaptor<AggregateReport> reportArg = ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(reportArg.capture()); |
| AggregateReport aggregateReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, aggregateReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_eventLevelFiltersFailToMatch_generateAggregateReportOnly() |
| throws DatastoreException, JSONException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"2\",\n" |
| + " \"priority\": \"2\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"key_1\": [\"value_11\"] \n" |
| + " }]\n" |
| + "}," |
| + "{\n" |
| + " \"trigger_data\": \"3\",\n" |
| + " \"priority\": \"3\",\n" |
| + " \"deduplication_key\": \"3\",\n" |
| + " \"filters\": [{\n" |
| + " \"key_1\": [\"value_21\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\"product\":[\"1234\",\"2345\"], \"key_1\": " |
| + "[\"value_1_y\", \"value_2_y\"]}") |
| .setId("sourceId") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void |
| performAttribution_aggregateReportsExceedsLimitPerDestination_insertsOnlyEventReport() |
| throws DatastoreException, JSONException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 1024; |
| int numEventReportPerDestination = 1023; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao, never()).insertAggregateReport(any()); |
| verify(mMeasurementDao).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttribution_maxAggregateReportsPerSourceFlagDisabled_shouldGenerateReport() |
| throws DatastoreException, JSONException { |
| // Disable flag for max aggregate reports per source. |
| when(mFlags.getMeasurementEnableMaxAggregateReportsPerSource()).thenReturn(false); |
| when(mFlags.getMeasurementMaxAggregateReportsPerSource()).thenReturn(20); |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 10; |
| int numEventReportPerDestination = 10; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| when(mMeasurementDao.getNumAggregateReportsPerSource(source.getId())).thenReturn(21); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttribution_aggregateReportsExceedsLimitPerSource_insertsOnlyEventReport() |
| throws DatastoreException, JSONException { |
| // Enable flag for max aggregate reports per source. |
| when(mFlags.getMeasurementEnableMaxAggregateReportsPerSource()).thenReturn(true); |
| when(mFlags.getMeasurementMaxAggregateReportsPerSource()).thenReturn(20); |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 10; |
| int numEventReportPerDestination = 10; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| when(mMeasurementDao.getNumAggregateReportsPerSource(source.getId())).thenReturn(21); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao, never()).insertAggregateReport(any()); |
| verify(mDebugReportApi) |
| .scheduleTriggerDebugReport( |
| any(), |
| any(), |
| eq("21"), |
| any(), |
| eq(DebugReportApi.Type.TRIGGER_AGGREGATE_EXCESSIVE_REPORTS)); |
| verify(mMeasurementDao).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttribution_eventReportsExceedsLimit_insertsOnlyAggregateReport() |
| throws DatastoreException, JSONException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 1023; |
| int numEventReportPerDestination = 1024; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttribution_aggregateAndEventReportsExceedsLimit_noReportInsertion() |
| throws DatastoreException, JSONException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 1024; |
| int numEventReportPerDestination = 1024; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttribution_aggregateAndEventReportsDoNotExceedsLimit_ReportInsertion() |
| throws DatastoreException, JSONException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"5\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 1023; |
| int numEventReportPerDestination = 1023; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create( |
| source.getAppDestinations(), |
| source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| // Verify event report registration origin. |
| ArgumentCaptor<EventReport> reportArg = ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao).insertEventReport(reportArg.capture()); |
| EventReport eventReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, eventReport.getRegistrationOrigin()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_withXnaConfig_originalSourceWinsAndOtherIgnored() |
| throws DatastoreException { |
| // Setup |
| String adtechEnrollment = "AdTech1-Ads"; |
| AttributionConfig attributionConfig = |
| new AttributionConfig.Builder() |
| .setSourceAdtech(adtechEnrollment) |
| .setSourcePriorityRange(new Pair<>(1L, 1000L)) |
| .setSourceFilters(null) |
| .setPriority(1L) |
| .setExpiry(604800L) |
| .setFilterData(null) |
| .build(); |
| Trigger trigger = |
| getXnaTriggerBuilder() |
| .setFilters(null) |
| .setNotFilters(null) |
| .setAttributionConfig( |
| new JSONArray( |
| Collections.singletonList( |
| attributionConfig.serializeAsJson(mFlags))) |
| .toString()) |
| .build(); |
| |
| String aggregatableSource = SourceFixture.ValidSourceParams.buildAggregateSource(); |
| Source xnaSource = |
| createXnaSourceBuilder() |
| .setEnrollmentId(adtechEnrollment) |
| // Priority changes to 1 for derived source |
| .setPriority(100L) |
| .setAggregateSource(aggregatableSource) |
| .setFilterData(null) |
| .setSharedAggregationKeys( |
| new JSONArray(Arrays.asList("campaignCounts", "geoValue")) |
| .toString()) |
| .build(); |
| // winner due to install attribution and higher priority |
| Source triggerEnrollmentSource1 = |
| createXnaSourceBuilder() |
| .setEnrollmentId(trigger.getEnrollmentId()) |
| .setPriority(2L) |
| .setFilterData(null) |
| .setAggregateSource(aggregatableSource) |
| .build(); |
| |
| Source triggerEnrollmentSource2 = |
| createXnaSourceBuilder() |
| .setEnrollmentId(trigger.getEnrollmentId()) |
| .setPriority(2L) |
| .setFilterData(null) |
| .setAggregateSource(aggregatableSource) |
| .setInstallAttributed(false) |
| .build(); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(xnaSource); |
| matchingSourceList.add(triggerEnrollmentSource1); |
| matchingSourceList.add(triggerEnrollmentSource2); |
| when(mMeasurementDao.fetchTriggerMatchingSourcesForXna(any(), any())) |
| .thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceDestinations(triggerEnrollmentSource1.getId())) |
| .thenReturn( |
| Pair.create( |
| triggerEnrollmentSource1.getAppDestinations(), |
| triggerEnrollmentSource1.getWebDestinations())); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mFlags.getMeasurementEnableXNA()).thenReturn(true); |
| |
| // Execution |
| boolean result = mHandler.performPendingAttributions(); |
| |
| // Assertion |
| assertTrue(result); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao).insertEventReport(any()); |
| verify(mMeasurementDao, times(2)).insertAttribution(any()); |
| verify(mMeasurementDao) |
| .insertIgnoredSourceForEnrollment(xnaSource.getId(), trigger.getEnrollmentId()); |
| verify(mMeasurementDao) |
| .updateSourceStatus( |
| eq(Collections.singletonList(triggerEnrollmentSource2.getId())), |
| eq(Source.Status.IGNORED)); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_withXnaConfig_derivedSourceWinsAndOtherIgnored() |
| throws DatastoreException { |
| // Setup |
| String adtechEnrollment = "AdTech1-Ads"; |
| AttributionConfig attributionConfig = |
| new AttributionConfig.Builder() |
| .setExpiry(604800L) |
| .setSourceAdtech(adtechEnrollment) |
| .setSourcePriorityRange(new Pair<>(1L, 1000L)) |
| .setSourceFilters(null) |
| .setPriority(50L) |
| .setExpiry(604800L) |
| .setFilterData(null) |
| .build(); |
| Trigger trigger = |
| getXnaTriggerBuilder() |
| .setFilters(null) |
| .setNotFilters(null) |
| .setAttributionConfig( |
| new JSONArray( |
| Collections.singletonList( |
| attributionConfig.serializeAsJson(mFlags))) |
| .toString()) |
| .build(); |
| |
| String aggregatableSource = SourceFixture.ValidSourceParams.buildAggregateSource(); |
| // Its derived source will be winner due to install attribution and higher priority |
| Source xnaSource = |
| createXnaSourceBuilder() |
| .setEnrollmentId(adtechEnrollment) |
| // Priority changes to 50 for derived source |
| .setPriority(1L) |
| .setAggregateSource(aggregatableSource) |
| .setFilterData(null) |
| .setSharedAggregationKeys( |
| new JSONArray(Arrays.asList("campaignCounts", "geoValue")) |
| .toString()) |
| .build(); |
| Source triggerEnrollmentSource1 = |
| createXnaSourceBuilder() |
| .setEnrollmentId(trigger.getEnrollmentId()) |
| .setPriority(2L) |
| .setFilterData(null) |
| .setAggregateSource(aggregatableSource) |
| .build(); |
| |
| Source triggerEnrollmentSource2 = |
| createXnaSourceBuilder() |
| .setEnrollmentId(trigger.getEnrollmentId()) |
| .setPriority(2L) |
| .setFilterData(null) |
| .setAggregateSource(aggregatableSource) |
| .setInstallAttributed(false) |
| .build(); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(xnaSource); |
| matchingSourceList.add(triggerEnrollmentSource1); |
| matchingSourceList.add(triggerEnrollmentSource2); |
| when(mMeasurementDao.fetchTriggerMatchingSourcesForXna(any(), any())) |
| .thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mFlags.getMeasurementEnableXNA()).thenReturn(true); |
| |
| // Execution |
| boolean result = mHandler.performPendingAttributions(); |
| |
| // Assertion |
| assertTrue(result); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| // Verify aggregate report registration origin. |
| ArgumentCaptor<AggregateReport> reportArg = ArgumentCaptor.forClass(AggregateReport.class); |
| verify(mMeasurementDao).insertAggregateReport(reportArg.capture()); |
| AggregateReport aggregateReport = reportArg.getValue(); |
| assertEquals(REGISTRATION_URI, aggregateReport.getRegistrationOrigin()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao).insertAttribution(any()); |
| verify(mMeasurementDao) |
| .updateSourceStatus( |
| eq( |
| Arrays.asList( |
| triggerEnrollmentSource1.getId(), |
| triggerEnrollmentSource2.getId())), |
| eq(Source.Status.IGNORED)); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttributions_xnaDisabled_derivedSourceIgnored() throws DatastoreException { |
| // Setup |
| String adtechEnrollment = "AdTech1-Ads"; |
| AttributionConfig attributionConfig = |
| new AttributionConfig.Builder() |
| .setExpiry(604800L) |
| .setSourceAdtech(adtechEnrollment) |
| .setSourcePriorityRange(new Pair<>(1L, 1000L)) |
| .setSourceFilters(null) |
| .setPriority(50L) |
| .setExpiry(604800L) |
| .setFilterData(null) |
| .build(); |
| Trigger trigger = |
| getXnaTriggerBuilder() |
| .setFilters(null) |
| .setNotFilters(null) |
| .setAttributionConfig( |
| new JSONArray( |
| Collections.singletonList( |
| attributionConfig.serializeAsJson(mFlags))) |
| .toString()) |
| .build(); |
| |
| String aggregatableSource = SourceFixture.ValidSourceParams.buildAggregateSource(); |
| // Its derived source will be winner due to install attribution and higher priority |
| Source xnaSource = |
| createXnaSourceBuilder() |
| .setEnrollmentId(adtechEnrollment) |
| // Priority changes to 50 for derived source |
| .setPriority(1L) |
| .setAggregateSource(aggregatableSource) |
| .setFilterData(null) |
| .setSharedAggregationKeys( |
| new JSONArray(Arrays.asList("campaignCounts", "geoValue")) |
| .toString()) |
| .build(); |
| Source triggerEnrollmentSource1 = |
| createXnaSourceBuilder() |
| .setEnrollmentId(trigger.getEnrollmentId()) |
| .setPriority(2L) |
| .setFilterData(null) |
| .setAggregateSource(aggregatableSource) |
| .build(); |
| |
| Source triggerEnrollmentSource2 = |
| createXnaSourceBuilder() |
| .setEnrollmentId(trigger.getEnrollmentId()) |
| .setPriority(2L) |
| .setFilterData(null) |
| .setAggregateSource(aggregatableSource) |
| .setInstallAttributed(false) |
| .build(); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(xnaSource); |
| matchingSourceList.add(triggerEnrollmentSource1); |
| matchingSourceList.add(triggerEnrollmentSource2); |
| when(mMeasurementDao.fetchTriggerMatchingSourcesForXna(any(), any())) |
| .thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mFlags.getMeasurementEnableXNA()).thenReturn(false); |
| |
| // Execution |
| boolean result = mHandler.performPendingAttributions(); |
| |
| // Assertion |
| assertTrue(result); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao, never()).insertAttribution(any()); |
| verify(mMeasurementDao, never()) |
| .updateSourceStatus( |
| eq( |
| Arrays.asList( |
| triggerEnrollmentSource1.getId(), |
| triggerEnrollmentSource2.getId())), |
| eq(Source.Status.IGNORED)); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| public void performAttribution_flexEventReport_oneTriggerGenerateTwoReports() |
| throws DatastoreException, JSONException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"1\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"value\": \"1000\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| TriggerSpecs triggerSpecs = SourceFixture.getValidTriggerSpecsValueSum(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .setTriggerSpecsString(triggerSpecs.encodeToJson()) |
| .setMaxEventLevelReports(triggerSpecs.getMaxReports()) |
| .setPrivacyParameters(triggerSpecs.encodePrivacyParametersToJSONString()) |
| .build(); |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 1023; |
| int numEventReportPerDestination = 1023; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, times(2)).insertEventReport(any()); |
| verify(mMeasurementDao, never()).fetchMatchingEventReports(any(), any()); |
| verify(mMeasurementDao, times(1)).getSourceEventReports(any()); |
| verify(mMeasurementDao, times(1)).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(source.attributedTriggersToJsonFlexApi())); |
| assertEquals(1, source.getTriggerSpecs().getAttributedTriggers().size()); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| /** |
| * The triggerData in the trigger didn't match any one of the trigger data in the source |
| * registration. No report generated |
| */ |
| public void performAttribution_flexEventReport_triggerDataMismatch() |
| throws DatastoreException, JSONException { |
| // Setup |
| long triggerTime = 234324L; |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"6\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"value\": \"1000\",\n" |
| + " \"deduplication_key\": \"1\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime(triggerTime) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| TriggerSpecs triggerSpecs = SourceFixture.getValidTriggerSpecsValueSum(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventReportWindow(triggerTime + 1L) |
| .setAggregatableReportWindow(triggerTime + 1L) |
| .setTriggerSpecsString(triggerSpecs.encodeToJson()) |
| .setMaxEventLevelReports(triggerSpecs.getMaxReports()) |
| .setPrivacyParameters(triggerSpecs.encodePrivacyParametersToJSONString()) |
| .build(); |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 1023; |
| int numEventReportPerDestination = 1023; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao, never()).fetchMatchingEventReports(any(), any()); |
| verify(mMeasurementDao, never()).getSourceEventReports(any()); |
| verify(mMeasurementDao, never()).updateSourceAttributedTriggers(anyString(), anyString()); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| } |
| |
| @Test |
| /** |
| * Status before attribution: 1 trigger attributed and 1 report generated; Incoming trigger |
| * status: 2 new reports should be generated; Result: 2 reports written into DB and no |
| * competition condition; debug reports and trigger debug keys populated only when all trigger |
| * contributors have debug keys. |
| */ |
| public void performAttribution_flexEventReportTwoNonCompetingTriggersAllDebugReports() |
| throws DatastoreException, JSONException { |
| // Setup |
| long baseTime = System.currentTimeMillis(); |
| UnsignedLong triggerData1 = new UnsignedLong(1L); |
| UnsignedLong triggerData2 = new UnsignedLong(2L); |
| UnsignedLong sourceDebugKey = new UnsignedLong(777L); |
| UnsignedLong debugKey1 = new UnsignedLong(4L); |
| UnsignedLong debugKey2 = new UnsignedLong(55L); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setDestinationType(EventSurfaceType.APP) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"" + triggerData2 + "\"," |
| + " \"priority\": \"123\"," |
| + " \"value\": \"105\"," |
| + " \"deduplication_key\": \"123\"" |
| + "}" |
| + "]") |
| .setFilters( |
| "[{" |
| + " \"key_1\": [\"value_1\", \"value_2\"]," |
| + " \"key_2\": [\"value_1\", \"value_2\"]" |
| + "}]") |
| .setTriggerTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(1) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .setAdIdPermission(true) |
| .setDebugKey(debugKey2) |
| .build(); |
| |
| Pair<Long, Long> firstBucket = Pair.create(10L, 99L); |
| Pair<Long, Long> secondBucket = Pair.create(100L, PENULTIMATE_INT_AS_LONG); |
| |
| final EventReport currentEventReport1 = |
| new EventReport.Builder() |
| .setId("100") |
| .setSourceEventId(new UnsignedLong(22L)) |
| .setEnrollmentId("another-enrollment-id") |
| .setAttributionDestinations(List.of(Uri.parse("android-app://com.ignored"))) |
| .setReportTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setStatus(EventReport.Status.PENDING) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRegistrationOrigin(WebUtil.validUri("https://adtech2.test")) |
| .setTriggerTime(baseTime + 3000L) |
| .setTriggerData(triggerData1) |
| .setTriggerPriority(123L) |
| .setTriggerValue(30) |
| .setTriggerSummaryBucket(firstBucket) |
| .setTriggerDedupKey(new UnsignedLong(3L)) |
| .setTriggerDebugKey(debugKey1) |
| .build(); |
| |
| TriggerSpecs templateTriggerSpecs = SourceFixture.getValidTriggerSpecsValueSum(); |
| JSONArray existingAttributes = new JSONArray(); |
| JSONObject triggerRecord1 = generateTriggerJSONFromEventReport(currentEventReport1); |
| |
| existingAttributes.put(triggerRecord1); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setEventTime(baseTime) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(2) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregatableReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(2) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setTriggerSpecsString(templateTriggerSpecs.encodeToJson()) |
| .setMaxEventLevelReports(templateTriggerSpecs.getMaxReports()) |
| .setEventAttributionStatus(existingAttributes.toString()) |
| .setPrivacyParameters( |
| templateTriggerSpecs.encodePrivacyParametersToJSONString()) |
| .setAdIdPermission(true) |
| .setDebugKey(sourceDebugKey) |
| .build(); |
| |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())) |
| .thenReturn(new ArrayList<>(Collections.singletonList(currentEventReport1))); |
| int numAggregateReportPerDestination = 10; |
| int numEventReportPerDestination = 10; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| |
| verify(mMeasurementDao, times(1)).getSourceEventReports(any()); |
| ArgumentCaptor<EventReport> insertedReportsCaptor = |
| ArgumentCaptor.forClass(EventReport.class); |
| ArgumentCaptor<EventReport> deletedReportsCaptor = |
| ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao, times(3)).insertEventReport(insertedReportsCaptor.capture()); |
| verify(mMeasurementDao, times(1)).deleteEventReport(deletedReportsCaptor.capture()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| List<EventReport> insertedEventReports = insertedReportsCaptor.getAllValues(); |
| assertEquals(firstBucket, insertedEventReports.get(0).getTriggerSummaryBucket()); |
| assertEquals(firstBucket, insertedEventReports.get(1).getTriggerSummaryBucket()); |
| assertEquals(secondBucket, insertedEventReports.get(2).getTriggerSummaryBucket()); |
| assertEquals(triggerData1, insertedEventReports.get(0).getTriggerData()); |
| assertEquals(triggerData2, insertedEventReports.get(1).getTriggerData()); |
| assertEquals(triggerData2, insertedEventReports.get(2).getTriggerData()); |
| assertEquals(List.of(debugKey1), insertedEventReports.get(0).getTriggerDebugKeys()); |
| assertEquals(List.of(debugKey2), insertedEventReports.get(1).getTriggerDebugKeys()); |
| assertEquals(List.of(debugKey2), insertedEventReports.get(2).getTriggerDebugKeys()); |
| assertNull(insertedEventReports.get(0).getSourceDebugKey()); |
| assertEquals(sourceDebugKey, insertedEventReports.get(1).getSourceDebugKey()); |
| assertEquals(sourceDebugKey, insertedEventReports.get(2).getSourceDebugKey()); |
| assertEquals( |
| EventReport.DebugReportStatus.NONE, |
| insertedEventReports.get(0).getDebugReportStatus()); |
| assertEquals( |
| EventReport.DebugReportStatus.PENDING, |
| insertedEventReports.get(1).getDebugReportStatus()); |
| assertEquals( |
| EventReport.DebugReportStatus.PENDING, |
| insertedEventReports.get(2).getDebugReportStatus()); |
| long reportTime = TimeUnit.DAYS.toMillis(2) + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS; |
| assertEquals(reportTime, insertedEventReports.get(0).getReportTime() - baseTime); |
| assertEquals(reportTime, insertedEventReports.get(1).getReportTime() - baseTime); |
| assertEquals(reportTime, insertedEventReports.get(2).getReportTime() - baseTime); |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).fetchMatchingEventReports(any(), any()); |
| verify(mMeasurementDao, times(1)).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(source.attributedTriggersToJsonFlexApi())); |
| assertEquals(2, source.getTriggerSpecs().getAttributedTriggers().size()); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| } |
| |
| @Test |
| /* |
| * Status before attribution: 1 trigger attributed and 2 report generated with triggerData 1; |
| * Incoming trigger status: 2 reports should be generated for triggerData 2 Result: incoming |
| * trigger has higher priority and one previous report should be deleted; debug reports and |
| * trigger debug keys populated only when all trigger contributors have debug keys. |
| */ |
| public void performAttribution_flexEventReportSecondTriggerHigherPriorityAllDebugReports() |
| throws DatastoreException, JSONException { |
| // Setup |
| long baseTime = System.currentTimeMillis(); |
| UnsignedLong triggerData1 = new UnsignedLong(1L); |
| UnsignedLong triggerData2 = new UnsignedLong(2L); |
| UnsignedLong sourceDebugKey = new UnsignedLong(777L); |
| UnsignedLong debugKey1 = new UnsignedLong(4L); |
| UnsignedLong debugKey2 = new UnsignedLong(55L); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[" |
| + "{" |
| + " \"trigger_data\": \"" + triggerData2 + "\"," |
| + " \"priority\": \"123\"," |
| + " \"value\": \"105\"," |
| + " \"deduplication_key\": \"1234\"" |
| + "}" |
| + "]") |
| .setFilters( |
| "[{" |
| + " \"key_1\": [\"value_1\", \"value_2\"]," |
| + " \"key_2\": [\"value_1\", \"value_2\"]" |
| + "}]") |
| .setTriggerTime(baseTime + TimeUnit.DAYS.toMillis(1) + 4800000) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .setAdIdPermission(true) |
| .setDebugKey(debugKey2) |
| .build(); |
| |
| final EventReport.Builder eventReportBuilder = |
| new EventReport.Builder() |
| .setId("100") |
| .setSourceEventId(new UnsignedLong(22L)) |
| .setEnrollmentId("another-enrollment-id") |
| .setAttributionDestinations(List.of(Uri.parse("https://bar.test"))) |
| .setReportTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(2) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setStatus(EventReport.Status.PENDING) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRegistrationOrigin(WebUtil.validUri("https://adtech2.test")) |
| .setTriggerTime(baseTime + TimeUnit.DAYS.toMillis(1)) |
| .setTriggerData(triggerData1) |
| .setTriggerPriority(121L) |
| .setTriggerValue(101) |
| .setTriggerDedupKey(new UnsignedLong(3L)) |
| .setTriggerDebugKey(debugKey1); |
| |
| TriggerSpecs templateTriggerSpecs = SourceFixture.getValidTriggerSpecsValueSum(); |
| JSONArray existingAttributes = new JSONArray(); |
| JSONObject triggerRecord1 = generateTriggerJSONFromEventReport(eventReportBuilder.build()); |
| |
| existingAttributes.put(triggerRecord1); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventReportDedupKeys( |
| new ArrayList<>(Collections.singletonList(new UnsignedLong(3L)))) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{" |
| + " \"key_1\": [\"value_1\", \"value_2\"]," |
| + " \"key_2\": [\"value_1\", \"value_2\"]" |
| + "}") |
| .setEventTime(baseTime) |
| .setEventReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregatableReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setTriggerSpecsString(templateTriggerSpecs.encodeToJson()) |
| .setMaxEventLevelReports(templateTriggerSpecs.getMaxReports()) |
| .setEventAttributionStatus(existingAttributes.toString()) |
| .setPrivacyParameters( |
| templateTriggerSpecs.encodePrivacyParametersToJSONString()) |
| .setAdIdPermission(true) |
| .setDebugKey(sourceDebugKey) |
| .build(); |
| |
| Pair<Long, Long> firstBucket = Pair.create(10L, 99L); |
| Pair<Long, Long> secondBucket = Pair.create(100L, PENULTIMATE_INT_AS_LONG); |
| |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())) |
| .thenReturn( |
| new ArrayList<>(Arrays.asList( |
| eventReportBuilder |
| .setTriggerSummaryBucket(firstBucket) |
| .build(), |
| eventReportBuilder |
| .setTriggerSummaryBucket(secondBucket) |
| .build()))); |
| int numAggregateReportPerDestination = 0; |
| int numEventReportPerDestination = 2; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, times(1)).getSourceEventReports(any()); |
| verify(mMeasurementDao, never()).fetchMatchingEventReports(any(), any()); |
| ArgumentCaptor<EventReport> insertedReportsCaptor = |
| ArgumentCaptor.forClass(EventReport.class); |
| ArgumentCaptor<EventReport> deletedReportsCaptor = |
| ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao, times(3)).insertEventReport(insertedReportsCaptor.capture()); |
| verify(mMeasurementDao, times(2)).deleteEventReport(deletedReportsCaptor.capture()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| List<EventReport> insertedEventReports = insertedReportsCaptor.getAllValues(); |
| assertEquals(firstBucket, insertedEventReports.get(0).getTriggerSummaryBucket()); |
| assertEquals(secondBucket, insertedEventReports.get(1).getTriggerSummaryBucket()); |
| assertEquals(firstBucket, insertedEventReports.get(2).getTriggerSummaryBucket()); |
| assertEquals(triggerData2, insertedEventReports.get(0).getTriggerData()); |
| assertEquals(triggerData2, insertedEventReports.get(1).getTriggerData()); |
| assertEquals(triggerData1, insertedEventReports.get(2).getTriggerData()); |
| assertEquals(List.of(debugKey2), insertedEventReports.get(0).getTriggerDebugKeys()); |
| assertEquals(List.of(debugKey2), insertedEventReports.get(1).getTriggerDebugKeys()); |
| assertEquals(List.of(debugKey1), insertedEventReports.get(2).getTriggerDebugKeys()); |
| assertEquals(sourceDebugKey, insertedEventReports.get(0).getSourceDebugKey()); |
| assertEquals(sourceDebugKey, insertedEventReports.get(1).getSourceDebugKey()); |
| assertNull(insertedEventReports.get(2).getSourceDebugKey()); |
| assertEquals( |
| EventReport.DebugReportStatus.PENDING, |
| insertedEventReports.get(0).getDebugReportStatus()); |
| assertEquals( |
| EventReport.DebugReportStatus.PENDING, |
| insertedEventReports.get(1).getDebugReportStatus()); |
| assertEquals( |
| EventReport.DebugReportStatus.NONE, |
| insertedEventReports.get(2).getDebugReportStatus()); |
| assertEquals(2, deletedReportsCaptor.getAllValues().size()); |
| long reportTime = TimeUnit.DAYS.toMillis(2) + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS; |
| assertEquals(reportTime, insertedEventReports.get(0).getReportTime() - baseTime); |
| assertEquals(reportTime, insertedEventReports.get(1).getReportTime() - baseTime); |
| assertEquals(reportTime, insertedEventReports.get(2).getReportTime() - baseTime); |
| verify(mMeasurementDao, times(1)).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(source.attributedTriggersToJsonFlexApi())); |
| assertEquals(2, source.getTriggerSpecs().getAttributedTriggers().size()); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| } |
| |
| @Test |
| /* |
| * Status before attribution: 1 trigger attributed and 2 report generated with triggerData 1; |
| * Incoming trigger status: 2 reports should be generated for triggerData 2 Result: incoming |
| * trigger has lower priority, no previous report should be deleted and only 1 new report |
| * inserted into DB; debug reports and trigger debug keys populated only when all trigger |
| * contributors have debug keys. |
| */ |
| public void performAttribution_flexEventReportSecondTriggerLowerPrioritySomeDebugReports() |
| throws DatastoreException, JSONException { |
| |
| // Setup |
| long baseTime = System.currentTimeMillis(); |
| UnsignedLong triggerData1 = new UnsignedLong(1L); |
| UnsignedLong triggerData2 = new UnsignedLong(2L); |
| UnsignedLong sourceDebugKey = new UnsignedLong(777L); |
| UnsignedLong debugKey2 = new UnsignedLong(55L); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[" |
| + "{" |
| + " \"trigger_data\": \"" + triggerData2 + "\"," |
| + " \"priority\": \"123\"," |
| + " \"value\": \"105\"," |
| + " \"deduplication_key\": \"1\"" |
| + "}" |
| + "]") |
| .setFilters( |
| "[{" |
| + " \"key_1\": [\"value_1\", \"value_2\"]," |
| + " \"key_2\": [\"value_1\", \"value_2\"]" |
| + "}]") |
| .setTriggerTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(1) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .setAdIdPermission(true) |
| .setDebugKey(debugKey2) |
| .build(); |
| |
| Pair<Long, Long> firstBucket = Pair.create(10L, 99L); |
| Pair<Long, Long> secondBucket = Pair.create(100L, PENULTIMATE_INT_AS_LONG); |
| |
| final EventReport.Builder eventReportBuilder = |
| new EventReport.Builder() |
| .setId("100") |
| .setSourceEventId(new UnsignedLong(22L)) |
| .setEnrollmentId("another-enrollment-id") |
| .setAttributionDestinations(List.of(Uri.parse("https://bar.test"))) |
| .setReportTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(2) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setStatus(EventReport.Status.PENDING) |
| .setDebugReportStatus(EventReport.DebugReportStatus.PENDING) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRegistrationOrigin(WebUtil.validUri("https://adtech2.test")) |
| .setTriggerTime(baseTime + 3000) |
| .setTriggerData(triggerData1) |
| .setTriggerPriority(124L) |
| .setSourceDebugKey(sourceDebugKey) |
| .setTriggerValue(103) |
| .setTriggerDedupKey(new UnsignedLong(3L)); |
| |
| TriggerSpecs templateTriggerSpecs = SourceFixture.getValidTriggerSpecsValueSum(); |
| JSONArray existingAttributes = new JSONArray(); |
| JSONObject triggerRecord1 = generateTriggerJSONFromEventReport(eventReportBuilder.build()); |
| |
| existingAttributes.put(triggerRecord1); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventReportDedupKeys( |
| new ArrayList<>(Collections.singleton(new UnsignedLong(3L)))) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventTime(baseTime) |
| .setEventReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregatableReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setTriggerSpecsString(templateTriggerSpecs.encodeToJson()) |
| .setMaxEventLevelReports(templateTriggerSpecs.getMaxReports()) |
| .setEventAttributionStatus(existingAttributes.toString()) |
| .setPrivacyParameters( |
| templateTriggerSpecs.encodePrivacyParametersToJSONString()) |
| .setAdIdPermission(true) |
| .setDebugKey(sourceDebugKey) |
| .build(); |
| |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())) |
| .thenReturn( |
| new ArrayList<>(Arrays.asList( |
| eventReportBuilder |
| .setTriggerSummaryBucket(firstBucket) |
| .build(), |
| eventReportBuilder |
| .setTriggerSummaryBucket(secondBucket) |
| .build()))); |
| int numAggregateReportPerDestination = 24; |
| int numEventReportPerDestination = 35; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| |
| verify(mMeasurementDao, times(1)).getSourceEventReports(any()); |
| ArgumentCaptor<EventReport> insertedReportsCaptor = |
| ArgumentCaptor.forClass(EventReport.class); |
| ArgumentCaptor<EventReport> deletedReportsCaptor = |
| ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao, times(3)).insertEventReport(insertedReportsCaptor.capture()); |
| verify(mMeasurementDao, times(2)).deleteEventReport(deletedReportsCaptor.capture()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| List<EventReport> insertedEventReports = insertedReportsCaptor.getAllValues(); |
| assertEquals(firstBucket, insertedEventReports.get(0).getTriggerSummaryBucket()); |
| assertEquals(secondBucket, insertedEventReports.get(1).getTriggerSummaryBucket()); |
| assertEquals(firstBucket, insertedEventReports.get(2).getTriggerSummaryBucket()); |
| assertEquals(triggerData1, insertedEventReports.get(0).getTriggerData()); |
| assertEquals(triggerData1, insertedEventReports.get(1).getTriggerData()); |
| assertEquals(triggerData2, insertedEventReports.get(2).getTriggerData()); |
| assertEquals(Collections.emptyList(), insertedEventReports.get(0).getTriggerDebugKeys()); |
| assertEquals(Collections.emptyList(), insertedEventReports.get(1).getTriggerDebugKeys()); |
| assertEquals(List.of(debugKey2), insertedEventReports.get(2).getTriggerDebugKeys()); |
| assertEquals(sourceDebugKey, insertedEventReports.get(0).getSourceDebugKey()); |
| assertEquals(sourceDebugKey, insertedEventReports.get(1).getSourceDebugKey()); |
| assertEquals(sourceDebugKey, insertedEventReports.get(2).getSourceDebugKey()); |
| assertEquals( |
| EventReport.DebugReportStatus.NONE, |
| insertedEventReports.get(0).getDebugReportStatus()); |
| assertEquals( |
| EventReport.DebugReportStatus.NONE, |
| insertedEventReports.get(1).getDebugReportStatus()); |
| assertEquals( |
| EventReport.DebugReportStatus.PENDING, |
| insertedEventReports.get(2).getDebugReportStatus()); |
| long reportTime = TimeUnit.DAYS.toMillis(2) + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS; |
| assertEquals(reportTime, insertedEventReports.get(0).getReportTime() - baseTime); |
| assertEquals(reportTime, insertedEventReports.get(1).getReportTime() - baseTime); |
| assertEquals(reportTime, insertedEventReports.get(2).getReportTime() - baseTime); |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).fetchMatchingEventReports(any(), any()); |
| verify(mMeasurementDao, times(1)).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(source.attributedTriggersToJsonFlexApi())); |
| assertEquals(2, source.getTriggerSpecs().getAttributedTriggers().size()); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| } |
| |
| @Test |
| /** |
| * Status before attribution: 2 trigger attributed and 2 report generated with triggerData 1 and |
| * 2, respectively; Incoming trigger status: 2 reports should be generated for triggerData 1 |
| * Result: incoming trigger has higher priority so previous report with triggerData 2 is |
| * deleted. |
| */ |
| public void performAttribution_flexEventReport_insertThirdTriggerPriorityReset() |
| throws DatastoreException, JSONException { |
| // Setup |
| long baseTime = System.currentTimeMillis(); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"1\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"value\": \"105\",\n" |
| + " \"deduplication_key\": \"111\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(1) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| |
| Pair<Long, Long> firstBucket = Pair.create(10L, 99L); |
| Pair<Long, Long> secondBucket = Pair.create(100L, PENULTIMATE_INT_AS_LONG); |
| |
| final EventReport currentEventReport1 = |
| new EventReport.Builder() |
| .setId("100") |
| .setTriggerId("01234") |
| .setSourceEventId(new UnsignedLong(22L)) |
| .setEnrollmentId("another-enrollment-id") |
| .setAttributionDestinations(List.of(Uri.parse("https://bar.test"))) |
| .setReportTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(2) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setStatus(EventReport.Status.PENDING) |
| .setDebugReportStatus(EventReport.DebugReportStatus.PENDING) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRegistrationOrigin(WebUtil.validUri("https://adtech2.test")) |
| .setTriggerTime(baseTime + TimeUnit.DAYS.toMillis(1)) |
| .setTriggerData(new UnsignedLong(1L)) |
| .setTriggerPriority(50) |
| .setTriggerValue(20) |
| .setTriggerSummaryBucket(firstBucket) |
| .setTriggerDedupKey(new UnsignedLong(3L)) |
| .build(); |
| final EventReport currentEventReport2 = |
| new EventReport.Builder() |
| .setId("101") |
| .setTriggerId("12345") |
| .setSourceEventId(new UnsignedLong(22L)) |
| .setEnrollmentId("another-enrollment-id") |
| .setAttributionDestinations(List.of(Uri.parse("https://bar.test"))) |
| .setReportTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(2) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setStatus(EventReport.Status.PENDING) |
| .setDebugReportStatus(EventReport.DebugReportStatus.PENDING) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRegistrationOrigin(WebUtil.validUri("https://adtech2.test")) |
| .setTriggerTime(baseTime + TimeUnit.DAYS.toMillis(1) + 1000) |
| .setTriggerData(new UnsignedLong(2L)) |
| .setTriggerPriority(60) |
| .setTriggerValue(30) |
| .setTriggerSummaryBucket(firstBucket) |
| .setTriggerDedupKey(new UnsignedLong(1233L)) |
| .build(); |
| |
| TriggerSpecs templateTriggerSpecs = new TriggerSpecs( |
| SourceFixture.getTriggerSpecValueSumArrayValidBaseline(), 2, null); |
| |
| JSONArray existingAttributes = new JSONArray(); |
| JSONObject triggerRecord1 = generateTriggerJSONFromEventReport(currentEventReport1); |
| JSONObject triggerRecord2 = generateTriggerJSONFromEventReport(currentEventReport2); |
| |
| existingAttributes.put(triggerRecord1); |
| existingAttributes.put(triggerRecord2); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventReportDedupKeys( |
| new ArrayList<>( |
| Arrays.asList( |
| new UnsignedLong(3L), new UnsignedLong(1233L)))) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventTime(baseTime) |
| .setEventReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregatableReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setTriggerSpecsString(templateTriggerSpecs.encodeToJson()) |
| .setMaxEventLevelReports(templateTriggerSpecs.getMaxReports()) |
| .setEventAttributionStatus(existingAttributes.toString()) |
| .setPrivacyParameters( |
| templateTriggerSpecs.encodePrivacyParametersToJSONString()) |
| .build(); |
| |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())) |
| .thenReturn( |
| new ArrayList<>(Arrays.asList(currentEventReport1, currentEventReport2))); |
| int numAggregateReportPerDestination = 3; |
| int numEventReportPerDestination = 3; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| |
| verify(mMeasurementDao, times(1)).getSourceEventReports(any()); |
| ArgumentCaptor<EventReport> insertedReportsCaptor = |
| ArgumentCaptor.forClass(EventReport.class); |
| ArgumentCaptor<EventReport> deletedReportsCaptor = |
| ArgumentCaptor.forClass(EventReport.class); |
| verify(mMeasurementDao, times(2)).insertEventReport(insertedReportsCaptor.capture()); |
| verify(mMeasurementDao, times(2)).deleteEventReport(deletedReportsCaptor.capture()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| List<EventReport> insertedEventReports = insertedReportsCaptor.getAllValues(); |
| assertEquals(firstBucket, insertedEventReports.get(0).getTriggerSummaryBucket()); |
| assertEquals(secondBucket, insertedEventReports.get(1).getTriggerSummaryBucket()); |
| assertEquals(new UnsignedLong(1L), insertedEventReports.get(0).getTriggerData()); |
| assertEquals(new UnsignedLong(1L), insertedEventReports.get(1).getTriggerData()); |
| long reportTime = TimeUnit.DAYS.toMillis(2) + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS; |
| assertEquals(reportTime, insertedEventReports.get(0).getReportTime() - baseTime); |
| assertEquals(reportTime, insertedEventReports.get(1).getReportTime() - baseTime); |
| |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).fetchMatchingEventReports(any(), any()); |
| verify(mMeasurementDao, times(1)).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(source.attributedTriggersToJsonFlexApi())); |
| assertEquals(3, source.getTriggerSpecs().getAttributedTriggers().size()); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| } |
| |
| @Test |
| public void performAttribution_flexEventReport_notReportDueToDedup() |
| throws DatastoreException, JSONException { |
| |
| // Setup |
| long baseTime = System.currentTimeMillis(); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"1\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"value\": \"105\",\n" |
| + " \"deduplication_key\": \"111\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(1) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| |
| final EventReport currentEventReport1 = |
| new EventReport.Builder() |
| .setId("100") |
| .setTriggerId("01234") |
| .setSourceEventId(new UnsignedLong(22L)) |
| .setEnrollmentId("another-enrollment-id") |
| .setAttributionDestinations(List.of(Uri.parse("https://bar.test"))) |
| .setReportTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(2) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setStatus(EventReport.Status.PENDING) |
| .setDebugReportStatus(EventReport.DebugReportStatus.PENDING) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRegistrationOrigin(WebUtil.validUri("https://adtech2.test")) |
| .setTriggerTime(baseTime + TimeUnit.DAYS.toMillis(1)) |
| .setTriggerData(new UnsignedLong(1L)) |
| .setTriggerPriority(50) |
| .setTriggerValue(20) |
| .setTriggerDedupKey(new UnsignedLong(111L)) |
| .build(); |
| final EventReport currentEventReport2 = |
| new EventReport.Builder() |
| .setId("101") |
| .setTriggerId("12345") |
| .setSourceEventId(new UnsignedLong(22L)) |
| .setEnrollmentId("another-enrollment-id") |
| .setAttributionDestinations(List.of(Uri.parse("https://bar.test"))) |
| .setReportTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(2) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setStatus(EventReport.Status.PENDING) |
| .setDebugReportStatus(EventReport.DebugReportStatus.PENDING) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRegistrationOrigin(WebUtil.validUri("https://adtech2.test")) |
| .setTriggerTime(baseTime + TimeUnit.DAYS.toMillis(1) + 1000) |
| .setTriggerData(new UnsignedLong(2L)) |
| .setTriggerPriority(60) |
| .setTriggerValue(30) |
| .setTriggerDedupKey(new UnsignedLong(123L)) |
| .build(); |
| |
| TriggerSpecs templateTriggerSpecs = new TriggerSpecs( |
| SourceFixture.getTriggerSpecValueSumArrayValidBaseline(), 2, null); |
| JSONArray existingAttributes = new JSONArray(); |
| JSONObject triggerRecord1 = generateTriggerJSONFromEventReport(currentEventReport1); |
| JSONObject triggerRecord2 = generateTriggerJSONFromEventReport(currentEventReport2); |
| |
| existingAttributes.put(triggerRecord1); |
| existingAttributes.put(triggerRecord2); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventReportDedupKeys( |
| new ArrayList<>( |
| Arrays.asList( |
| new UnsignedLong(111L), new UnsignedLong(123L)))) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventTime(baseTime) |
| .setEventReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregatableReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setTriggerSpecsString(templateTriggerSpecs.encodeToJson()) |
| .setMaxEventLevelReports(templateTriggerSpecs.getMaxReports()) |
| .setEventAttributionStatus(existingAttributes.toString()) |
| .setPrivacyParameters( |
| templateTriggerSpecs.encodePrivacyParametersToJSONString()) |
| .build(); |
| |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())) |
| .thenReturn( |
| new ArrayList<>(Arrays.asList(currentEventReport1, currentEventReport2))); |
| int numAggregateReportPerDestination = 1023; |
| int numEventReportPerDestination = 1023; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).fetchMatchingEventReports(any(), any()); |
| verify(mMeasurementDao, never()).getSourceEventReports(any()); |
| |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao, never()).deleteEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| |
| verify(mMeasurementDao, never()).updateSourceAttributedTriggers(anyString(), anyString()); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| } |
| |
| @Test |
| public void performAttribution_flexEventReport_dedupKeyInserted() |
| throws DatastoreException, JSONException { |
| // Setup |
| long baseTime = System.currentTimeMillis(); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"1\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"value\": \"1\",\n" |
| + " \"deduplication_key\": \"111\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(1) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| |
| TriggerSpecs templateTriggerSpecs = new TriggerSpecs( |
| SourceFixture.getTriggerSpecValueSumArrayValidBaseline(), 2, null); |
| JSONArray existingAttributes = new JSONArray(); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventReportDedupKeys(new ArrayList<>()) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventTime(baseTime) |
| .setEventReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregatableReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setTriggerSpecsString(templateTriggerSpecs.encodeToJson()) |
| .setMaxEventLevelReports(templateTriggerSpecs.getMaxReports()) |
| .setEventAttributionStatus(existingAttributes.toString()) |
| .setPrivacyParameters( |
| templateTriggerSpecs.encodePrivacyParametersToJSONString()) |
| .build(); |
| |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 1023; |
| int numEventReportPerDestination = 1023; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).fetchMatchingEventReports(any(), any()); |
| verify(mMeasurementDao, times(1)).getSourceEventReports(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao, never()).deleteEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| |
| verify(mMeasurementDao, times(1)).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(source.attributedTriggersToJsonFlexApi())); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| } |
| |
| @Test |
| public void performAttribution_flexEventReport_dedupKeyInserted_dedupAlignflagOff() |
| throws DatastoreException, JSONException { |
| // Setup |
| long baseTime = System.currentTimeMillis(); |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"1\",\n" |
| + " \"priority\": \"123\",\n" |
| + " \"value\": \"1\",\n" |
| + " \"deduplication_key\": \"111\"\n" |
| + "}" |
| + "]\n") |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}]\n") |
| .setTriggerTime( |
| baseTime |
| + TimeUnit.DAYS.toMillis(1) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregateTriggerData(buildAggregateTriggerData().toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}") |
| .build(); |
| |
| TriggerSpecs templateTriggerSpecs = new TriggerSpecs( |
| SourceFixture.getTriggerSpecValueSumArrayValidBaseline(), 2, null); |
| JSONArray existingAttributes = new JSONArray(); |
| |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setEventReportDedupKeys(new ArrayList<>()) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource( |
| "{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData( |
| "{\n" |
| + " \"key_1\": [\"value_1\", \"value_2\"],\n" |
| + " \"key_2\": [\"value_1\", \"value_2\"]\n" |
| + "}\n") |
| .setEventTime(baseTime) |
| .setEventReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setAggregatableReportWindow( |
| baseTime |
| + TimeUnit.DAYS.toMillis(3) |
| + MEASUREMENT_MIN_EVENT_REPORT_DELAY_MILLIS) |
| .setTriggerSpecsString(templateTriggerSpecs.encodeToJson()) |
| .setMaxEventLevelReports(templateTriggerSpecs.getMaxReports()) |
| .setEventAttributionStatus(existingAttributes.toString()) |
| .setPrivacyParameters( |
| templateTriggerSpecs.encodePrivacyParametersToJSONString()) |
| .build(); |
| |
| when(mFlags.getMeasurementFlexibleEventReportingApiEnabled()).thenReturn(true); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| int numAggregateReportPerDestination = 5; |
| int numEventReportPerDestination = 4; |
| when(mMeasurementDao.getNumAggregateReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numAggregateReportPerDestination); |
| when(mMeasurementDao.getNumEventReportsPerDestination( |
| trigger.getAttributionDestination(), trigger.getDestinationType())) |
| .thenReturn(numEventReportPerDestination); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| when(mFlags.getMeasurementEnableAraDeduplicationAlignmentV1()).thenReturn(false); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertion |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| |
| verify(mMeasurementDao).insertAggregateReport(any()); |
| verify(mMeasurementDao, never()).fetchMatchingEventReports(any(), any()); |
| verify(mMeasurementDao, times(1)).getSourceEventReports(any()); |
| verify(mMeasurementDao, times(1)).updateSourceEventReportDedupKeys(any()); |
| verify(mMeasurementDao, never()).insertEventReport(any()); |
| verify(mMeasurementDao, never()).deleteEventReport(any()); |
| verify(mTransaction, times(2)).begin(); |
| verify(mTransaction, times(2)).end(); |
| |
| verify(mMeasurementDao, times(1)).updateSourceAttributedTriggers( |
| eq(source.getId()), eq(source.attributedTriggersToJsonFlexApi())); |
| verify(mMeasurementDao, never()).updateSourceStatus(any(), anyInt()); |
| } |
| |
| @Test |
| public void performAttributions_withXnaConfig_derivedSourceWinsAndIsLogged() |
| throws DatastoreException { |
| // Setup |
| String adtechEnrollment = "AdTech1-Ads"; |
| AttributionConfig attributionConfig = |
| new AttributionConfig.Builder() |
| .setExpiry(604800L) |
| .setSourceAdtech(adtechEnrollment) |
| .setSourcePriorityRange(new Pair<>(1L, 1000L)) |
| .setSourceFilters(null) |
| .setPriority(50L) |
| .setExpiry(604800L) |
| .setFilterData(null) |
| .build(); |
| Trigger trigger = |
| getXnaTriggerBuilder() |
| .setFilters(null) |
| .setNotFilters(null) |
| .setAttributionConfig( |
| new JSONArray( |
| Collections.singletonList( |
| attributionConfig.serializeAsJson(mFlags))) |
| .toString()) |
| .build(); |
| |
| String aggregatableSource = SourceFixture.ValidSourceParams.buildAggregateSource(); |
| // Its derived source will be winner due to install attribution and higher priority |
| Source xnaSource = |
| createXnaSourceBuilder() |
| .setEnrollmentId(adtechEnrollment) |
| // Priority changes to 50 for derived source |
| .setPriority(1L) |
| .setAggregateSource(aggregatableSource) |
| .setFilterData(null) |
| .setSharedAggregationKeys( |
| new JSONArray(Arrays.asList("campaignCounts", "geoValue")) |
| .toString()) |
| .build(); |
| Source triggerEnrollmentSource1 = |
| createXnaSourceBuilder() |
| .setEnrollmentId(trigger.getEnrollmentId()) |
| .setPriority(2L) |
| .setFilterData(null) |
| .setAggregateSource(aggregatableSource) |
| .build(); |
| |
| Source triggerEnrollmentSource2 = |
| createXnaSourceBuilder() |
| .setEnrollmentId(trigger.getEnrollmentId()) |
| .setPriority(2L) |
| .setFilterData(null) |
| .setAggregateSource(aggregatableSource) |
| .setInstallAttributed(false) |
| .build(); |
| |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(xnaSource); |
| matchingSourceList.add(triggerEnrollmentSource1); |
| matchingSourceList.add(triggerEnrollmentSource2); |
| when(mMeasurementDao.fetchTriggerMatchingSourcesForXna(any(), any())) |
| .thenReturn(matchingSourceList); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| when(mFlags.getMeasurementEnableXNA()).thenReturn(true); |
| |
| // Execution |
| boolean result = mHandler.performPendingAttributions(); |
| |
| // Assertion |
| assertTrue(result); |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| |
| ArgumentCaptor<MeasurementAttributionStats> statusArg = |
| ArgumentCaptor.forClass(MeasurementAttributionStats.class); |
| verify(mLogger).logMeasurementAttributionStats(statusArg.capture()); |
| MeasurementAttributionStats measurementAttributionStats = statusArg.getValue(); |
| assertTrue(measurementAttributionStats.isSourceDerived()); |
| } |
| |
| @Test |
| public void performAttributions_withinLookbackWindow_attributeTrigger() |
| throws DatastoreException { |
| when(mFlags.getMeasurementEnableLookbackWindowFilter()).thenReturn(true); |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"2\",\n" |
| + " \"priority\": \"2\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"source_type\": [\"event\"], \n" |
| + " \"dummy_key\": [\"dummy_value\"] \n" |
| + " }]\n" |
| + "}," |
| + "{\n" |
| + " \"trigger_data\": \"3\",\n" |
| + " \"priority\": \"3\",\n" |
| + " \"deduplication_key\": \"3\",\n" |
| + " \"filters\": [{\n" |
| + " \"source_type\": [\"navigation\"], \n" |
| + " \"dummy_key\": [\"dummy_value\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .setTriggerTime(TRIGGER_TIME) |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11\", \"value_12\"],\n" |
| // Set Lookback window to be greater than duration from |
| // source to trigger time. |
| + " \"_lookback_window\": 1000\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" + " \"key_1\": [\"value_11\", \"value_12\"]\n" + "}\n") |
| .setId("sourceId") |
| .setEventTime(TRIGGER_TIME - TimeUnit.SECONDS.toMillis(LOOKBACK_WINDOW - 1)) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setEventReportWindow(TRIGGER_TIME + 1) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1) |
| .build(); |
| EventReport expectedEventReport = |
| new EventReport.Builder() |
| .setTriggerPriority(3L) |
| .setTriggerDedupKey(new UnsignedLong(3L)) |
| .setTriggerData(new UnsignedLong(3L)) |
| .setTriggerTime(trigger.getTriggerTime()) |
| .setSourceEventId(source.getEventId()) |
| .setStatus(EventReport.Status.PENDING) |
| .setAttributionDestinations(source.getAppDestinations()) |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setReportTime( |
| mEventReportWindowCalcDelegate.getReportingTime( |
| source, |
| trigger.getTriggerTime(), |
| trigger.getDestinationType())) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRandomizedTriggerRate( |
| mSourceNoiseHandler.getRandomAttributionProbability(source)) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), |
| eq(Trigger.Status.ATTRIBUTED)); |
| String expectedAttributionStatus = |
| getAttributionStatus(List.of(trigger.getId()), List.of("3"), List.of("3")); |
| verify(mMeasurementDao) |
| .updateSourceAttributedTriggers(eq(source.getId()), eq(expectedAttributionStatus)); |
| verify(mMeasurementDao).insertEventReport(eq(expectedEventReport)); |
| } |
| |
| @Test |
| public void performAttributions_outsideLookbackWindow_noAttributionTrigger() |
| throws DatastoreException { |
| when(mFlags.getMeasurementEnableLookbackWindowFilter()).thenReturn(true); |
| // Setup |
| Trigger trigger = |
| TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers( |
| "[\n" |
| + "{\n" |
| + " \"trigger_data\": \"2\",\n" |
| + " \"priority\": \"2\",\n" |
| + " \"deduplication_key\": \"2\",\n" |
| + " \"filters\": [{\n" |
| + " \"source_type\": [\"event\"], \n" |
| + " \"dummy_key\": [\"dummy_value\"] \n" |
| + " }]\n" |
| + "}," |
| + "{\n" |
| + " \"trigger_data\": \"3\",\n" |
| + " \"priority\": \"3\",\n" |
| + " \"deduplication_key\": \"3\",\n" |
| + " \"filters\": [{\n" |
| + " \"source_type\": [\"navigation\"], \n" |
| + " \"dummy_key\": [\"dummy_value\"] \n" |
| + " }]\n" |
| + "}" |
| + "]\n") |
| .setTriggerTime(TRIGGER_TIME) |
| .setFilters( |
| "[{\n" |
| + " \"key_1\": [\"value_11\", \"value_12\"],\n" |
| // Set Lookback window to be smaller than duration from |
| // source to trigger time. |
| + " \"_lookback_window\": 1000\n" |
| + "}]\n") |
| .build(); |
| Source source = |
| SourceFixture.getMinimalValidSourceBuilder() |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setFilterData( |
| "{\n" + " \"key_1\": [\"value_11\", \"value_12\"]\n" + "}\n") |
| .setId("sourceId") |
| .setEventTime(TRIGGER_TIME - TimeUnit.SECONDS.toMillis(LOOKBACK_WINDOW + 1)) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setEventReportWindow(TRIGGER_TIME + 1) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1) |
| .build(); |
| EventReport expectedEventReport = |
| new EventReport.Builder() |
| .setTriggerPriority(2L) |
| .setTriggerDedupKey(new UnsignedLong(2L)) |
| .setTriggerData(new UnsignedLong(2L)) |
| .setTriggerTime(trigger.getTriggerTime()) |
| .setSourceEventId(source.getEventId()) |
| .setStatus(EventReport.Status.PENDING) |
| .setAttributionDestinations(source.getAppDestinations()) |
| .setEnrollmentId(source.getEnrollmentId()) |
| .setReportTime( |
| mEventReportWindowCalcDelegate.getReportingTime( |
| source, |
| trigger.getTriggerTime(), |
| trigger.getDestinationType())) |
| .setSourceType(Source.SourceType.NAVIGATION) |
| .setRandomizedTriggerRate( |
| mSourceNoiseHandler.getRandomAttributionProbability(source)) |
| .setSourceId(source.getId()) |
| .setTriggerId(trigger.getId()) |
| .setRegistrationOrigin(REGISTRATION_URI) |
| .build(); |
| when(mMeasurementDao.getPendingTriggerIds()) |
| .thenReturn(Collections.singletonList(trigger.getId())); |
| when(mMeasurementDao.getTrigger(trigger.getId())).thenReturn(trigger); |
| List<Source> matchingSourceList = new ArrayList<>(); |
| matchingSourceList.add(source); |
| when(mMeasurementDao.getMatchingActiveSources(trigger)).thenReturn(matchingSourceList); |
| when(mMeasurementDao.getSourceDestinations(source.getId())) |
| .thenReturn(Pair.create(source.getAppDestinations(), source.getWebDestinations())); |
| when(mMeasurementDao.getAttributionsPerRateLimitWindow( |
| anyInt(), any(), any())).thenReturn(5L); |
| when(mMeasurementDao.getSourceEventReports(any())).thenReturn(new ArrayList<>()); |
| |
| // Execution |
| mHandler.performPendingAttributions(); |
| |
| // Assertions |
| verify(mMeasurementDao) |
| .updateTriggerStatus( |
| eq(Collections.singletonList(trigger.getId())), eq(Trigger.Status.IGNORED)); |
| verify(mMeasurementDao, never()).updateSourceAttributedTriggers(any(), any()); |
| verify(mMeasurementDao, never()).insertEventReport(eq(expectedEventReport)); |
| } |
| |
| public static Trigger.Builder getXnaTriggerBuilder() { |
| return new Trigger.Builder() |
| .setId(UUID.randomUUID().toString()) |
| .setAttributionDestination( |
| TriggerFixture.ValidTriggerParams.ATTRIBUTION_DESTINATION) |
| .setEnrollmentId(TriggerFixture.ValidTriggerParams.ENROLLMENT_ID) |
| .setRegistrant(TriggerFixture.ValidTriggerParams.REGISTRANT) |
| .setTriggerTime(TriggerFixture.ValidTriggerParams.TRIGGER_TIME) |
| .setEventTriggers(TriggerFixture.ValidTriggerParams.EVENT_TRIGGERS) |
| .setAggregateTriggerData(TriggerFixture.ValidTriggerParams.AGGREGATE_TRIGGER_DATA) |
| .setAggregateValues(TriggerFixture.ValidTriggerParams.AGGREGATE_VALUES) |
| .setFilters(TriggerFixture.ValidTriggerParams.TOP_LEVEL_FILTERS_JSON_STRING) |
| .setNotFilters(TriggerFixture.ValidTriggerParams.TOP_LEVEL_NOT_FILTERS_JSON_STRING) |
| .setAttributionConfig(TriggerFixture.ValidTriggerParams.ATTRIBUTION_CONFIGS_STRING) |
| .setAdtechBitMapping(TriggerFixture.ValidTriggerParams.X_NETWORK_KEY_MAPPING) |
| .setRegistrationOrigin(TriggerFixture.ValidTriggerParams.REGISTRATION_ORIGIN); |
| } |
| |
| private Source.Builder createXnaSourceBuilder() { |
| return new Source.Builder() |
| .setId(UUID.randomUUID().toString()) |
| .setEventId(SourceFixture.ValidSourceParams.SOURCE_EVENT_ID) |
| .setPublisher(SourceFixture.ValidSourceParams.PUBLISHER) |
| .setAppDestinations(SourceFixture.ValidSourceParams.ATTRIBUTION_DESTINATIONS) |
| .setWebDestinations(SourceFixture.ValidSourceParams.WEB_DESTINATIONS) |
| .setEnrollmentId(SourceFixture.ValidSourceParams.ENROLLMENT_ID) |
| .setRegistrant(SourceFixture.ValidSourceParams.REGISTRANT) |
| .setEventTime(SourceFixture.ValidSourceParams.SOURCE_EVENT_TIME) |
| .setExpiryTime(SourceFixture.ValidSourceParams.EXPIRY_TIME) |
| .setPriority(SourceFixture.ValidSourceParams.PRIORITY) |
| .setSourceType(SourceFixture.ValidSourceParams.SOURCE_TYPE) |
| .setInstallAttributionWindow( |
| SourceFixture.ValidSourceParams.INSTALL_ATTRIBUTION_WINDOW) |
| .setInstallCooldownWindow(SourceFixture.ValidSourceParams.INSTALL_COOLDOWN_WINDOW) |
| .setAttributionMode(SourceFixture.ValidSourceParams.ATTRIBUTION_MODE) |
| .setAggregateSource(SourceFixture.ValidSourceParams.buildAggregateSource()) |
| .setFilterData(buildMatchingFilterData()) |
| .setIsDebugReporting(true) |
| .setRegistrationId(SourceFixture.ValidSourceParams.REGISTRATION_ID) |
| .setSharedAggregationKeys(SourceFixture.ValidSourceParams.SHARED_AGGREGATE_KEYS) |
| .setInstallTime(SourceFixture.ValidSourceParams.INSTALL_TIME) |
| .setAggregatableReportWindow(SourceFixture.ValidSourceParams.EXPIRY_TIME) |
| .setRegistrationOrigin(SourceFixture.ValidSourceParams.REGISTRATION_ORIGIN) |
| .setInstallAttributed(true); |
| } |
| |
| private String buildMatchingFilterData() { |
| try { |
| JSONObject filterMap = new JSONObject(); |
| filterMap.put( |
| "conversion_subdomain", |
| new JSONArray(Collections.singletonList("electronics.megastore"))); |
| return filterMap.toString(); |
| } catch (JSONException e) { |
| LogUtil.e("JSONException when building aggregate filter data."); |
| } |
| return null; |
| } |
| |
| private JSONArray buildAggregateTriggerData() throws JSONException { |
| JSONArray triggerDatas = new JSONArray(); |
| JSONObject jsonObject1 = new JSONObject(); |
| jsonObject1.put("key_piece", "0x400"); |
| jsonObject1.put("source_keys", new JSONArray(Arrays.asList("campaignCounts"))); |
| jsonObject1.put("filters", createFilterJSONArray()); |
| jsonObject1.put("not_filters", createFilterJSONArray()); |
| JSONObject jsonObject2 = new JSONObject(); |
| jsonObject2.put("key_piece", "0xA80"); |
| jsonObject2.put("source_keys", new JSONArray(Arrays.asList("geoValue", "noMatch"))); |
| triggerDatas.put(jsonObject1); |
| triggerDatas.put(jsonObject2); |
| return triggerDatas; |
| } |
| |
| private JSONArray createFilterJSONArray() throws JSONException { |
| JSONObject filterMap = new JSONObject(); |
| filterMap.put("conversion_subdomain", |
| new JSONArray(Arrays.asList("electronics.megastore"))); |
| filterMap.put("product", new JSONArray(Arrays.asList("1234", "2345"))); |
| JSONArray filterSet = new JSONArray(); |
| filterSet.put(filterMap); |
| return filterSet; |
| } |
| |
| private void assertNullAggregateReport( |
| AggregateReport report, Trigger trigger, long timeOffset) { |
| assertEquals(trigger.getRegistrationOrigin(), report.getRegistrationOrigin()); |
| assertEquals(trigger.getAttributionDestination(), report.getAttributionDestination()); |
| |
| long lowerBound = trigger.getTriggerTime() + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY; |
| // Add slightly more delay to upper bound to account for execution. |
| long upperBound = |
| trigger.getTriggerTime() |
| + PrivacyParams.AGGREGATE_REPORT_MIN_DELAY |
| + PrivacyParams.AGGREGATE_REPORT_DELAY_SPAN |
| + 1000L; |
| |
| assertTrue( |
| report.getScheduledReportTime() > lowerBound |
| && report.getScheduledReportTime() < upperBound); |
| |
| assertNull(report.getSourceDebugKey()); |
| assertEquals(trigger.getDebugKey(), report.getTriggerDebugKey()); |
| if (trigger.getAggregationCoordinatorOrigin() == null) { |
| assertEquals( |
| Uri.parse(AdServicesConfig.getMeasurementDefaultAggregationCoordinatorOrigin()), |
| report.getAggregationCoordinatorOrigin()); |
| } else { |
| assertEquals( |
| trigger.getAggregationCoordinatorOrigin(), |
| report.getAggregationCoordinatorOrigin()); |
| } |
| assertTrue(report.isFakeReport()); |
| assertEquals(trigger.getId(), report.getTriggerId()); |
| assertEquals(trigger.getTriggerTime() - timeOffset, report.getSourceRegistrationTime()); |
| } |
| |
| private void assertAggregateReportsEqual( |
| AggregateReport expectedReport, AggregateReport actualReport) { |
| // Avoids checking report time because there is randomization |
| assertEquals(expectedReport.getApiVersion(), actualReport.getApiVersion()); |
| assertEquals( |
| expectedReport.getAttributionDestination(), |
| actualReport.getAttributionDestination()); |
| assertEquals( |
| expectedReport.getDebugCleartextPayload(), actualReport.getDebugCleartextPayload()); |
| assertEquals(expectedReport.getEnrollmentId(), actualReport.getEnrollmentId()); |
| assertEquals(expectedReport.getPublisher(), actualReport.getPublisher()); |
| assertEquals(expectedReport.getSourceId(), actualReport.getSourceId()); |
| assertEquals(expectedReport.getTriggerId(), actualReport.getTriggerId()); |
| assertEquals( |
| expectedReport.getAggregateAttributionData(), |
| actualReport.getAggregateAttributionData()); |
| assertEquals(expectedReport.getSourceDebugKey(), actualReport.getSourceDebugKey()); |
| assertEquals(expectedReport.getTriggerDebugKey(), actualReport.getTriggerDebugKey()); |
| assertEquals(expectedReport.getRegistrationOrigin(), actualReport.getRegistrationOrigin()); |
| assertEquals(expectedReport.isFakeReport(), actualReport.isFakeReport()); |
| } |
| |
| private static Trigger.Builder getTriggerBuilder() throws JSONException { |
| return TriggerFixture.getValidTriggerBuilder() |
| .setId("triggerId1") |
| .setTriggerTime(TRIGGER_TIME) |
| .setStatus(Trigger.Status.PENDING) |
| .setEventTriggers(new JSONArray().toString()); |
| } |
| |
| private static Trigger.Builder getAggregateTriggerBuilder() throws JSONException { |
| JSONArray triggerDatas = new JSONArray(); |
| JSONObject jsonObject1 = new JSONObject(); |
| jsonObject1.put("key_piece", "0x400"); |
| jsonObject1.put("source_keys", new JSONArray(Arrays.asList("campaignCounts"))); |
| JSONObject jsonObject2 = new JSONObject(); |
| jsonObject2.put("key_piece", "0xA80"); |
| jsonObject2.put("source_keys", new JSONArray(Arrays.asList("geoValue", "noMatch"))); |
| triggerDatas.put(jsonObject1); |
| triggerDatas.put(jsonObject2); |
| |
| return getTriggerBuilder() |
| .setAggregateTriggerData(triggerDatas.toString()) |
| .setAggregateValues("{\"campaignCounts\":32768,\"geoValue\":1644}"); |
| } |
| |
| private static Trigger getAggregateTrigger() throws JSONException { |
| return getAggregateTriggerBuilder().build(); |
| } |
| |
| private static Trigger getAggregateAndEventTrigger() throws JSONException { |
| return getAggregateTriggerBuilder() |
| .setEventTriggers(EVENT_TRIGGERS) |
| .build(); |
| } |
| |
| private static Source getAggregateSource() { |
| return SourceFixture.getMinimalValidSourceBuilder() |
| .setId("sourceId1") |
| .setEventTime(SOURCE_TIME) |
| .setExpiryTime(EXPIRY_TIME) |
| .setAggregatableReportWindow(TRIGGER_TIME + 1L) |
| .setAttributionMode(Source.AttributionMode.TRUTHFULLY) |
| .setAggregateSource("{\"campaignCounts\" : \"0x159\", \"geoValue\" : \"0x5\"}") |
| .setFilterData("{\"product\":[\"1234\",\"2345\"]}") |
| .build(); |
| } |
| |
| private static JSONObject generateTriggerJSONFromEventReport(EventReport eventReport) |
| throws JSONException { |
| JSONObject triggerRecord = new JSONObject(); |
| triggerRecord.put("trigger_id", eventReport.getId()); |
| triggerRecord.put("value", eventReport.getTriggerValue()); |
| triggerRecord.put("priority", eventReport.getTriggerPriority()); |
| triggerRecord.put("trigger_time", eventReport.getTriggerTime()); |
| triggerRecord.put("trigger_data", eventReport.getTriggerData()); |
| triggerRecord.put("dedup_key", eventReport.getTriggerDedupKey()); |
| if (eventReport.getTriggerDebugKey() != null) { |
| triggerRecord.put("debug_key", eventReport.getTriggerDebugKey()); |
| } |
| triggerRecord.put("has_source_debug_key", eventReport.getSourceDebugKey() != null); |
| return triggerRecord; |
| } |
| |
| private static String getAttributionStatus(List<String> triggerIds, List<String> triggerData, |
| List<String> dedupKeys) { |
| try { |
| JSONArray attributionStatus = new JSONArray(); |
| for (int i = 0; i < triggerIds.size(); i++) { |
| attributionStatus.put( |
| new JSONObject() |
| .put("trigger_id", triggerIds.get(i)) |
| .put("trigger_data", triggerData.get(i)) |
| .put("dedup_key", dedupKeys.get(i))); |
| } |
| return attributionStatus.toString(); |
| } catch (JSONException ignored) { |
| return null; |
| } |
| } |
| |
| private static long roundDownToDay(long timestamp) { |
| return Math.floorDiv(timestamp, TimeUnit.DAYS.toMillis(1)) * TimeUnit.DAYS.toMillis(1); |
| } |
| } |