Split private and non-private observation generation

Private and non-private observation generation is generalized such the
aggregation procedures for each only need to provide a way to encode
aggregated values as observations.

Bug: 303091672

Test: atest AdServicesCobaltUnitTests

Change-Id: I6d85e7d4ccd98dd0523b59c165f8583c6b6a598d
diff --git a/adservices/libraries/cobalt/java/com/android/cobalt/impl/CobaltPeriodicJobImpl.java b/adservices/libraries/cobalt/java/com/android/cobalt/impl/CobaltPeriodicJobImpl.java
index e74111b..30a8e14 100644
--- a/adservices/libraries/cobalt/java/com/android/cobalt/impl/CobaltPeriodicJobImpl.java
+++ b/adservices/libraries/cobalt/java/com/android/cobalt/impl/CobaltPeriodicJobImpl.java
@@ -31,7 +31,7 @@
 import com.android.cobalt.data.ObservationStoreEntity;
 import com.android.cobalt.data.ReportKey;
 import com.android.cobalt.domain.Project;
-import com.android.cobalt.observations.CountObservationGenerator;
+import com.android.cobalt.observations.ObservationGeneratorFactory;
 import com.android.cobalt.observations.PrivacyGenerator;
 import com.android.cobalt.system.CobaltClock;
 import com.android.cobalt.system.SystemClock;
@@ -46,7 +46,6 @@
 import com.google.cobalt.ObservationMetadata;
 import com.google.cobalt.ReleaseStage;
 import com.google.cobalt.ReportDefinition;
-import com.google.cobalt.ReportDefinition.ReportType;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.util.concurrent.FluentFuture;
@@ -87,12 +86,10 @@
     private final Duration mUploadDoneDelay;
     private final SystemClock mSystemClock;
     private final boolean mEnabled;
-    private final SystemData mSystemData;
-    private final PrivacyGenerator mPrivacyGenerator;
-    private final SecureRandom mSecureRandom;
     private final Uploader mUploader;
     private final Encrypter mEncrypter;
     private final ByteString mApiKey;
+    private final ObservationGeneratorFactory mObservationGeneratorFactory;
 
     public CobaltPeriodicJobImpl(
             @NonNull Project project,
@@ -118,12 +115,15 @@
         mUploadDoneDelay = Objects.requireNonNull(uploadDoneDelay);
         mSystemClock = Objects.requireNonNull(systemClock);
         mEnabled = enabled;
-        mSystemData = Objects.requireNonNull(systemData);
-        mPrivacyGenerator = Objects.requireNonNull(privacyGenerator);
-        mSecureRandom = Objects.requireNonNull(secureRandom);
         mUploader = Objects.requireNonNull(uploader);
         mEncrypter = Objects.requireNonNull(encrypter);
         mApiKey = Objects.requireNonNull(apiKey);
+        mObservationGeneratorFactory =
+                new ObservationGeneratorFactory(
+                        mProject,
+                        Objects.requireNonNull(systemData),
+                        Objects.requireNonNull(privacyGenerator),
+                        Objects.requireNonNull(secureRandom));
     }
 
     /**
@@ -180,23 +180,11 @@
                                 metric.getId(),
                                 report.getId());
                 relevantReports.add(reportKey);
-                if (report.getReportType() != ReportType.FLEETWIDE_OCCURRENCE_COUNTS) {
-                    // Skip observation generation after recording the report is relevant in case
-                    // a disabled report may be enabled again.
-                    continue;
-                }
                 logInfo(
                         "Generating observations for day %s for report %s",
                         dayIndexToGenerate, reportKey);
                 ObservationGenerator generator =
-                        new CountObservationGenerator(
-                                mSystemData,
-                                mPrivacyGenerator,
-                                mSecureRandom,
-                                mProject.getCustomerId(),
-                                mProject.getProjectId(),
-                                metric,
-                                report);
+                        mObservationGeneratorFactory.getObservationGenerator(metric, report);
                 results.add(
                         mDataService.generateCountObservations(
                                 reportKey, dayIndexToGenerate, dayIndexLoggerEnabled, generator));
diff --git a/adservices/libraries/cobalt/java/com/android/cobalt/observations/CountObservationGenerator.java b/adservices/libraries/cobalt/java/com/android/cobalt/observations/CountObservationGenerator.java
deleted file mode 100644
index ce393e5..0000000
--- a/adservices/libraries/cobalt/java/com/android/cobalt/observations/CountObservationGenerator.java
+++ /dev/null
@@ -1,290 +0,0 @@
-/*
- * Copyright (C) 2023 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.cobalt.observations;
-
-import static com.android.cobalt.collect.ImmutableHelpers.toImmutableList;
-
-import android.annotation.NonNull;
-
-import com.android.cobalt.data.EventRecordAndSystemProfile;
-import com.android.cobalt.data.ObservationGenerator;
-import com.android.cobalt.system.SystemData;
-
-import com.google.cobalt.IntegerObservation;
-import com.google.cobalt.MetricDefinition;
-import com.google.cobalt.Observation;
-import com.google.cobalt.ObservationMetadata;
-import com.google.cobalt.ObservationToEncrypt;
-import com.google.cobalt.PrivateIndexObservation;
-import com.google.cobalt.ReportDefinition;
-import com.google.cobalt.ReportDefinition.PrivacyLevel;
-import com.google.cobalt.ReportParticipationObservation;
-import com.google.cobalt.SystemProfile;
-import com.google.cobalt.UnencryptedObservationBatch;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableListMultimap;
-import com.google.protobuf.ByteString;
-
-import java.security.SecureRandom;
-import java.util.Objects;
-
-/** Observation generator for FLEETWIDE_OCCURRENCE_COUNTS. */
-public final class CountObservationGenerator implements ObservationGenerator {
-    private final SystemData mSystemData;
-    private final PrivacyGenerator mPrivacyGenerator;
-    private final SecureRandom mSecureRandom;
-    private final int mCustomerId;
-    private final int mProjectId;
-    private final MetricDefinition mMetric;
-    private final ReportDefinition mReport;
-
-    public CountObservationGenerator(
-            @NonNull SystemData systemData,
-            @NonNull PrivacyGenerator privacyGenerator,
-            @NonNull SecureRandom secureRandom,
-            int customerId,
-            int projectId,
-            @NonNull MetricDefinition metric,
-            @NonNull ReportDefinition report) {
-
-        this.mSystemData = Objects.requireNonNull(systemData);
-        this.mPrivacyGenerator = Objects.requireNonNull(privacyGenerator);
-        this.mSecureRandom = Objects.requireNonNull(secureRandom);
-        this.mCustomerId = customerId;
-        this.mProjectId = projectId;
-        this.mMetric = Objects.requireNonNull(metric);
-        this.mReport = Objects.requireNonNull(report);
-    }
-
-    /**
-     * Generate the observations that occurred for a single day and report.
-     *
-     * @param dayIndex the day to generate observations for
-     * @param allEventData per-system profile event data being aggregated
-     * @return observations to be stored in the database for later sending
-     */
-    @Override
-    public ImmutableList<UnencryptedObservationBatch> generateObservations(
-            int dayIndex,
-            ImmutableListMultimap<SystemProfile, EventRecordAndSystemProfile> allEventData) {
-        if (allEventData.isEmpty() && mReport.getPrivacyLevel() != PrivacyLevel.NO_ADDED_PRIVACY) {
-            // Reports with privacy enabled need to send fabricated observations and a report
-            // participation observation even if no real observations are present.
-            return ImmutableList.of(
-                    generateObservations(
-                            dayIndex,
-                            // Use the current system profile since none is provided.
-                            mSystemData.filteredSystemProfile(mReport),
-                            ImmutableList.of()));
-        }
-
-        return allEventData.keySet().stream()
-                .map(
-                        systemProfile ->
-                                generateObservations(
-                                        dayIndex, systemProfile, allEventData.get(systemProfile)))
-                .collect(toImmutableList());
-    }
-
-    /**
-     * Generate an observation batch from events for a given day and system profile.
-     *
-     * @param dayIndex the day observations are being generated for
-     * @param systemProfile the system profile of the observations
-     * @param eventData the events
-     * @return an UnencryptedObservation batch holding the generated observations
-     */
-    private UnencryptedObservationBatch generateObservations(
-            int dayIndex,
-            SystemProfile systemProfile,
-            ImmutableList<EventRecordAndSystemProfile> eventData) {
-        if (mReport.getEventVectorBufferMax() != 0
-                && eventData.size() > mReport.getEventVectorBufferMax()) {
-            // Each EventRecordAndSystemProfile contains a unique event vector for the system
-            // profile and day so the number of events can be compared to the event vector buffer
-            // max of the report.
-            eventData = eventData.subList(0, (int) mReport.getEventVectorBufferMax());
-        }
-
-        ImmutableList<ObservationToEncrypt> observations =
-                mReport.getPrivacyLevel() != PrivacyLevel.NO_ADDED_PRIVACY
-                        ? buildPrivateObservations(eventData)
-                        : buildNonPrivateObservations(eventData);
-
-        return UnencryptedObservationBatch.newBuilder()
-                .setMetadata(
-                        ObservationMetadata.newBuilder()
-                                .setCustomerId(mCustomerId)
-                                .setProjectId(mProjectId)
-                                .setMetricId(mMetric.getId())
-                                .setReportId(mReport.getId())
-                                .setDayIndex(dayIndex)
-                                .setSystemProfile(systemProfile))
-                .addAllUnencryptedObservations(observations)
-                .build();
-    }
-
-    /** Securely generate 8 random bytes. */
-    private ByteString generateSecureRandomByteString() {
-        byte[] randomId = new byte[8];
-        mSecureRandom.nextBytes(randomId);
-        return ByteString.copyFrom(randomId);
-    }
-
-    /**
-     * Build a list of non-private observations from event data.
-     *
-     * @param eventData the event data to encode
-     * @return ObservationToEncrypts containing non-private observations
-     */
-    private ImmutableList<ObservationToEncrypt> buildNonPrivateObservations(
-            ImmutableList<EventRecordAndSystemProfile> eventData) {
-        IntegerObservation.Builder integerBuilder = IntegerObservation.newBuilder();
-        eventData.stream().map(this::buildIntegerValue).forEach(integerBuilder::addValues);
-        Observation observation =
-                Observation.newBuilder()
-                        .setInteger(integerBuilder)
-                        .setRandomId(generateSecureRandomByteString())
-                        .build();
-        return ImmutableList.of(
-                // Reports without privacy only make a single contribution so the id is set.
-                buildObservationToEncrypt(observation, /* setContributionId= */ true));
-    }
-
-    /**
-     * Build an intger observation value from an event vector and aggregate value in an event.
-     *
-     * @param countEvent the event
-     * @return an IntegerObservation.Value
-     */
-    private IntegerObservation.Value buildIntegerValue(EventRecordAndSystemProfile event) {
-        return IntegerObservation.Value.newBuilder()
-                .addAllEventCodes(event.eventVector().eventCodes())
-                .setValue(event.aggregateValue().getIntegerValue())
-                .build();
-    }
-
-    /**
-     * Build a list of private observations to encrypt from a set of event indices.
-     *
-     * @param eventData the events which occurred
-     * @return a list of observations to encrypt, including fabricated observations
-     */
-    private ImmutableList<ObservationToEncrypt> buildPrivateObservations(
-            ImmutableList<EventRecordAndSystemProfile> eventData) {
-        ImmutableList<Integer> eventIndices =
-                eventData.stream()
-                        .map(countEvent -> asIndex(countEvent))
-                        .collect(toImmutableList());
-        ImmutableList<Integer> allIndices =
-                mPrivacyGenerator.addNoise(eventIndices, maxIndexForReport(), mReport);
-        ImmutableList<Observation> observations =
-                ImmutableList.<Observation>builder()
-                        .addAll(allIndices.stream().map(i -> buildPrivateObservation(i)).iterator())
-                        .add(buildParticipationObservation())
-                        .build();
-        ImmutableList.Builder<ObservationToEncrypt> toEncrypt = ImmutableList.builder();
-        boolean setContributionId = true;
-        for (int i = 0; i < observations.size(); ++i) {
-            // Reports with privacy enabled split a single contribution across multiple
-            // observations, both private and participation. However, only 1 needs the
-            // contribution id set.
-            toEncrypt.add(buildObservationToEncrypt(observations.get(i), setContributionId));
-            setContributionId = false;
-        }
-
-        return toEncrypt.build();
-    }
-
-    /**
-     * Turn an index into an observation.
-     *
-     * @param index the private index
-     * @return an Observation that contains a private observation
-     */
-    private Observation buildPrivateObservation(int index) {
-        return Observation.newBuilder()
-                .setPrivateIndex(PrivateIndexObservation.newBuilder().setIndex(index).build())
-                .setRandomId(generateSecureRandomByteString())
-                .build();
-    }
-
-    /**
-     * Create a report participation observation.
-     *
-     * @return an Observation that contains a report participation observation
-     */
-    private Observation buildParticipationObservation() {
-        return Observation.newBuilder()
-                .setReportParticipation(ReportParticipationObservation.getDefaultInstance())
-                .setRandomId(generateSecureRandomByteString())
-                .build();
-    }
-
-    /**
-     * Create an observation to encrypt and optionally set the contribution id
-     *
-     * @param observation the observation
-     * @param setContributionId whether the contribution id should be set
-     * @return an ObservationToEncrypt
-     */
-    private ObservationToEncrypt buildObservationToEncrypt(
-            Observation observation, boolean setContributionId) {
-        ObservationToEncrypt.Builder builder = ObservationToEncrypt.newBuilder();
-        builder.setObservation(observation);
-        if (setContributionId) {
-            builder.setContributionId(generateSecureRandomByteString());
-        }
-
-        return builder.build();
-    }
-
-    /**
-     * Convert an event into a private index.
-     *
-     * @param event the event to convert
-     * @return the index of the event
-     */
-    private int asIndex(EventRecordAndSystemProfile event) {
-        int maxEventVectorIndex = maxEventVectorIndexForMetric();
-        int eventVectorIndex =
-                PrivateIndexCalculations.eventVectorToIndex(event.eventVector(), mMetric);
-
-        long clippedValue =
-                PrivateIndexCalculations.clipValue(
-                        event.aggregateValue().getIntegerValue(), mReport);
-        int clippedValueIndex =
-                PrivateIndexCalculations.longToIndex(
-                        clippedValue,
-                        mReport.getMinValue(),
-                        mReport.getMaxValue(),
-                        mReport.getNumIndexPoints(),
-                        mSecureRandom);
-        return PrivateIndexCalculations.valueAndEventVectorIndicesToIndex(
-                clippedValueIndex, eventVectorIndex, maxEventVectorIndex);
-    }
-
-    private int maxIndexForReport() {
-        return PrivateIndexCalculations.getNumEventVectors(mMetric.getMetricDimensionsList())
-                        * mReport.getNumIndexPoints()
-                - 1;
-    }
-
-    private int maxEventVectorIndexForMetric() {
-        return PrivateIndexCalculations.getNumEventVectors(mMetric.getMetricDimensionsList()) - 1;
-    }
-}
diff --git a/adservices/libraries/cobalt/java/com/android/cobalt/observations/IntegerEncoder.java b/adservices/libraries/cobalt/java/com/android/cobalt/observations/IntegerEncoder.java
new file mode 100644
index 0000000..8e7e23a
--- /dev/null
+++ b/adservices/libraries/cobalt/java/com/android/cobalt/observations/IntegerEncoder.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+
+import com.android.cobalt.data.EventRecordAndSystemProfile;
+
+import com.google.cobalt.IntegerObservation;
+import com.google.cobalt.Observation;
+import com.google.common.collect.ImmutableList;
+
+import java.security.SecureRandom;
+
+/**
+ * Encodes events for the same day and report as a single {@link Observation} that wraps an {@link
+ * IntegerObservation}.
+ *
+ * <p>Note, this encoder expects input {@link AggregateValue} objects to have an inner integer value
+ * set and will use 0 if not.
+ */
+final class IntegerEncoder implements NonPrivateObservationGenerator.Encoder {
+    private final SecureRandom mSecureRandom;
+
+    IntegerEncoder(@NonNull SecureRandom secureRandom) {
+        this.mSecureRandom = requireNonNull(secureRandom);
+    }
+
+    /**
+     * Encodes integer events for the same day and report as a single {@link IntegerObservation}.
+     *
+     * @param events the events to encode
+     * @return an observation which wraps an {@link IntegerObservation} holding the input events
+     */
+    @Override
+    public Observation encode(ImmutableList<EventRecordAndSystemProfile> events) {
+        IntegerObservation.Builder integerObservation = IntegerObservation.newBuilder();
+        for (EventRecordAndSystemProfile event : events) {
+            IntegerObservation.Value value =
+                    IntegerObservation.Value.newBuilder()
+                            .addAllEventCodes(event.eventVector().eventCodes())
+                            .setValue(event.aggregateValue().getIntegerValue())
+                            .build();
+            integerObservation.addValues(value);
+        }
+        return Observation.newBuilder()
+                .setInteger(integerObservation)
+                .setRandomId(RandomId.generate(mSecureRandom))
+                .build();
+    }
+}
diff --git a/adservices/libraries/cobalt/java/com/android/cobalt/observations/NonPrivateObservationGenerator.java b/adservices/libraries/cobalt/java/com/android/cobalt/observations/NonPrivateObservationGenerator.java
new file mode 100644
index 0000000..9564f4c
--- /dev/null
+++ b/adservices/libraries/cobalt/java/com/android/cobalt/observations/NonPrivateObservationGenerator.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+
+import com.android.cobalt.data.EventRecordAndSystemProfile;
+import com.android.cobalt.data.ObservationGenerator;
+
+import com.google.cobalt.Observation;
+import com.google.cobalt.ObservationMetadata;
+import com.google.cobalt.ObservationToEncrypt;
+import com.google.cobalt.ReportDefinition;
+import com.google.cobalt.SystemProfile;
+import com.google.cobalt.UnencryptedObservationBatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+
+import java.security.SecureRandom;
+import java.util.Collection;
+import java.util.Map;
+
+/** Generates non-private observations from event data. */
+final class NonPrivateObservationGenerator implements ObservationGenerator {
+    /** Interface to encode aggregated values as observations for non-private reports. */
+    interface Encoder {
+        /**
+         * Encodes multiple events for the same report, day, and system profile as a single
+         * non-private observation.
+         *
+         * @param events the events that need to be encoded
+         * @return a non-private observation that contains all input event data
+         */
+        Observation encode(ImmutableList<EventRecordAndSystemProfile> events);
+    }
+
+    private final SecureRandom mSecureRandom;
+    private final Encoder mEncoder;
+    private final int mCustomerId;
+    private final int mProjectId;
+    private final int mMetricId;
+    private final ReportDefinition mReport;
+
+    NonPrivateObservationGenerator(
+            @NonNull SecureRandom secureRandom,
+            @NonNull Encoder encoder,
+            int customerId,
+            int projectId,
+            int metricId,
+            ReportDefinition report) {
+        this.mSecureRandom = requireNonNull(secureRandom);
+        this.mEncoder = requireNonNull(encoder);
+        this.mCustomerId = customerId;
+        this.mProjectId = projectId;
+        this.mMetricId = metricId;
+        this.mReport = report;
+    }
+
+    /**
+     * Generate the non-private observations that occurred for a report and day.
+     *
+     * @param dayIndex the day index to generate observations for
+     * @param allEventData the data for events that occurred that are relevant to the day and Report
+     * @return the observations to store in the DB for later sending, contained in
+     *     UnencryptedObservationBatches with their metadata
+     */
+    @Override
+    public ImmutableList<UnencryptedObservationBatch> generateObservations(
+            int dayIndex,
+            ImmutableListMultimap<SystemProfile, EventRecordAndSystemProfile> allEventData) {
+        ImmutableList.Builder<UnencryptedObservationBatch> batches = ImmutableList.builder();
+        for (Map.Entry<SystemProfile, Collection<EventRecordAndSystemProfile>> eventData :
+                allEventData.asMap().entrySet()) {
+            SystemProfile systemProfile = eventData.getKey();
+            ImmutableList<EventRecordAndSystemProfile> events =
+                    ImmutableList.copyOf(eventData.getValue());
+            if (mReport.getEventVectorBufferMax() != 0
+                    && events.size() > mReport.getEventVectorBufferMax()) {
+                // Each EventRecordAndSystemProfile contains a unique event vector for the
+                // system profile and day so the number of events can be compared to the event
+                // vector buffer max of the report.
+                events = events.subList(0, (int) mReport.getEventVectorBufferMax());
+            }
+
+            ObservationToEncrypt observation =
+                    ObservationToEncrypt.newBuilder()
+                            .setObservation(mEncoder.encode(events))
+                            .setContributionId(RandomId.generate(mSecureRandom))
+                            .build();
+            UnencryptedObservationBatch.Builder batch =
+                    UnencryptedObservationBatch.newBuilder()
+                            .setMetadata(
+                                    ObservationMetadata.newBuilder()
+                                            .setCustomerId(mCustomerId)
+                                            .setProjectId(mProjectId)
+                                            .setMetricId(mMetricId)
+                                            .setReportId(mReport.getId())
+                                            .setDayIndex(dayIndex)
+                                            .setSystemProfile(systemProfile))
+                            .addUnencryptedObservations(observation);
+            batches.add(batch.build());
+        }
+        return batches.build();
+    }
+}
diff --git a/adservices/libraries/cobalt/java/com/android/cobalt/observations/ObservationGeneratorFactory.java b/adservices/libraries/cobalt/java/com/android/cobalt/observations/ObservationGeneratorFactory.java
new file mode 100644
index 0000000..01a8088
--- /dev/null
+++ b/adservices/libraries/cobalt/java/com/android/cobalt/observations/ObservationGeneratorFactory.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+
+import com.android.cobalt.data.ObservationGenerator;
+import com.android.cobalt.domain.Project;
+import com.android.cobalt.system.SystemData;
+
+import com.google.cobalt.MetricDefinition;
+import com.google.cobalt.ReportDefinition;
+import com.google.cobalt.ReportDefinition.ReportType;
+
+import java.security.SecureRandom;
+
+/** Creates {@link ObservationGenerator} instances for reports. */
+public final class ObservationGeneratorFactory {
+    private final Project mProject;
+    private final SystemData mSystemData;
+    private final PrivacyGenerator mPrivacyGenerator;
+    private final SecureRandom mSecureRandom;
+
+    public ObservationGeneratorFactory(
+            @NonNull Project project,
+            @NonNull SystemData systemData,
+            @NonNull PrivacyGenerator privacyGenerator,
+            @NonNull SecureRandom secureRandom) {
+        mProject = requireNonNull(project);
+        mSystemData = requireNonNull(systemData);
+        mPrivacyGenerator = requireNonNull(privacyGenerator);
+        mSecureRandom = requireNonNull(secureRandom);
+    }
+
+    /**
+     * Creates an {@link ObservationGenerator} instance for a report.
+     *
+     * <p>Note, only FLEETWIDE_OCCURRENCE_COUNTS are supported.
+     *
+     * @param metric the metric observations are being generated for
+     * @param report the metric observations are being generated for
+     * @return the {@link ObservationGenerator} required for the provided report
+     */
+    public ObservationGenerator getObservationGenerator(
+            MetricDefinition metric, ReportDefinition report) {
+        if (report.getReportType() != ReportType.FLEETWIDE_OCCURRENCE_COUNTS) {
+            throw new AssertionError(
+                    "Unrecognized or unsupported report type: " + report.getReportTypeValue());
+        }
+
+        switch (report.getPrivacyLevel()) {
+            case NO_ADDED_PRIVACY:
+                return new NonPrivateObservationGenerator(
+                        mSecureRandom,
+                        new IntegerEncoder(mSecureRandom),
+                        mProject.getCustomerId(),
+                        mProject.getProjectId(),
+                        metric.getId(),
+                        report);
+            case LOW_PRIVACY:
+            case MEDIUM_PRIVACY:
+            case HIGH_PRIVACY:
+                return new PrivateObservationGenerator(
+                        mSystemData,
+                        mPrivacyGenerator,
+                        mSecureRandom,
+                        new PrivateIntegerEncoder(mSecureRandom, metric, report),
+                        mProject.getCustomerId(),
+                        mProject.getProjectId(),
+                        metric,
+                        report);
+            default:
+                throw new AssertionError(
+                        "Unknown or unset privacy level: " + report.getPrivacyLevelValue());
+        }
+    }
+}
diff --git a/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivacyGenerator.java b/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivacyGenerator.java
index 5ce9e31..0399b78 100644
--- a/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivacyGenerator.java
+++ b/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivacyGenerator.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 
+import com.google.cobalt.PrivateIndexObservation;
 import com.google.cobalt.ReportDefinition;
 import com.google.common.collect.ImmutableList;
 import com.google.common.math.BigDecimalMath;
@@ -35,17 +36,14 @@
     }
 
     /**
-     * Adds noise to a list of private indices.
+     * Generates noise for a report.
      *
-     * <p>Each private index is an observation which actually occurred.
-     *
-     * @param indices the private indices
      * @param maxIndex the maximum private index value for the report
      * @param reportDefinition the privacy-enabled report containing the privacy parameters
      * @return private indices that include noise according to the report's privacy parameters
      */
-    ImmutableList<Integer> addNoise(
-            ImmutableList<Integer> indices, int maxIndex, ReportDefinition reportDefinition) {
+    ImmutableList<PrivateIndexObservation> generateNoise(
+            int maxIndex, ReportDefinition reportDefinition) {
         checkArgument(maxIndex >= 0, "maxIndex value cannot be negative");
         double lambda = reportDefinition.getPoissonMean();
         checkArgument(lambda > 0, "poisson_mean must be positive, got %s", lambda);
@@ -66,13 +64,15 @@
 
         int addedOnes = samplePoissonDistribution(lambdaTimesNumIndex);
 
-        ImmutableList.Builder<Integer> withNoise = ImmutableList.<Integer>builder();
-        withNoise.addAll(indices);
+        ImmutableList.Builder<PrivateIndexObservation> noise = ImmutableList.builder();
         for (int i = 0; i < addedOnes; ++i) {
-            withNoise.add(sampleUniformDistribution(maxIndex));
+            noise.add(
+                    PrivateIndexObservation.newBuilder()
+                            .setIndex(sampleUniformDistribution(maxIndex))
+                            .build());
         }
 
-        return withNoise.build();
+        return noise.build();
     }
 
     /**
diff --git a/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivateIntegerEncoder.java b/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivateIntegerEncoder.java
new file mode 100644
index 0000000..b175071
--- /dev/null
+++ b/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivateIntegerEncoder.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+
+import com.android.cobalt.data.EventVector;
+
+import com.google.cobalt.AggregateValue;
+import com.google.cobalt.MetricDefinition;
+import com.google.cobalt.PrivateIndexObservation;
+import com.google.cobalt.ReportDefinition;
+
+import java.security.SecureRandom;
+
+/**
+ * Encodes an integer event as an {@link PrivateIndexObservation}.
+ *
+ * <p>Note, this encoder expects input {@link AggregateValue} objects to have an inner integer value
+ * set and will use 0 if not.
+ */
+final class PrivateIntegerEncoder implements PrivateObservationGenerator.Encoder {
+    private final MetricDefinition mMetric;
+    private final ReportDefinition mReport;
+    private final SecureRandom mSecureRandom;
+
+    PrivateIntegerEncoder(
+            @NonNull SecureRandom secureRandom,
+            @NonNull MetricDefinition metric,
+            @NonNull ReportDefinition report) {
+        this.mSecureRandom = requireNonNull(secureRandom);
+        this.mMetric = requireNonNull(metric);
+        this.mReport = requireNonNull(report);
+    }
+
+    /**
+     * Encodes one event and aggregated value as a single private observation.
+     *
+     * @param eventVector the event vector to encode
+     * @param aggregateValue the aggregated value to encode
+     * @return the privacy encoded observation
+     */
+    @Override
+    public PrivateIndexObservation encode(EventVector eventVector, AggregateValue aggregateValue) {
+        int maxEventVectorIndex =
+                PrivateIndexCalculations.getNumEventVectors(mMetric.getMetricDimensionsList()) - 1;
+        int eventVectorIndex = PrivateIndexCalculations.eventVectorToIndex(eventVector, mMetric);
+
+        long clippedValue =
+                PrivateIndexCalculations.clipValue(aggregateValue.getIntegerValue(), mReport);
+        int clippedValueIndex =
+                PrivateIndexCalculations.longToIndex(
+                        clippedValue,
+                        mReport.getMinValue(),
+                        mReport.getMaxValue(),
+                        mReport.getNumIndexPoints(),
+                        mSecureRandom);
+        int privateIndex =
+                PrivateIndexCalculations.valueAndEventVectorIndicesToIndex(
+                        clippedValueIndex, eventVectorIndex, maxEventVectorIndex);
+        return PrivateIndexObservation.newBuilder().setIndex(privateIndex).build();
+    }
+}
diff --git a/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivateObservationGenerator.java b/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivateObservationGenerator.java
new file mode 100644
index 0000000..caa377d
--- /dev/null
+++ b/adservices/libraries/cobalt/java/com/android/cobalt/observations/PrivateObservationGenerator.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+
+import com.android.cobalt.data.EventRecordAndSystemProfile;
+import com.android.cobalt.data.EventVector;
+import com.android.cobalt.data.ObservationGenerator;
+import com.android.cobalt.system.SystemData;
+
+import com.google.cobalt.AggregateValue;
+import com.google.cobalt.MetricDefinition;
+import com.google.cobalt.Observation;
+import com.google.cobalt.ObservationMetadata;
+import com.google.cobalt.ObservationToEncrypt;
+import com.google.cobalt.PrivateIndexObservation;
+import com.google.cobalt.ReportDefinition;
+import com.google.cobalt.ReportParticipationObservation;
+import com.google.cobalt.SystemProfile;
+import com.google.cobalt.UnencryptedObservationBatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+
+import java.security.SecureRandom;
+
+/** Generates private observations from event data and report privacy parameters. */
+final class PrivateObservationGenerator implements ObservationGenerator {
+    /**
+     * Interface to encode an aggregated value as a private index observation for private reports.
+     */
+    interface Encoder {
+        /**
+         * Encodes one event and aggregated value as a single private observation.
+         *
+         * <p>Note, retuning a single private observation implies that report types that have
+         * multiple values in their {@link AggregateValue}, like histograms, aren't supported.
+         *
+         * @param eventVector the event vector to encode
+         * @param aggregateValue the aggregated value to encode
+         * @return the privacy encoded observation
+         */
+        PrivateIndexObservation encode(EventVector eventVector, AggregateValue aggregateValue);
+    }
+
+    private final SystemData mSystemData;
+    private final PrivacyGenerator mPrivacyGenerator;
+    private final SecureRandom mSecureRandom;
+    private final Encoder mEncoder;
+    private final int mCustomerId;
+    private final int mProjectId;
+    private final MetricDefinition mMetric;
+    private final ReportDefinition mReport;
+
+    PrivateObservationGenerator(
+            @NonNull SystemData systemData,
+            @NonNull PrivacyGenerator privacyGenerator,
+            @NonNull SecureRandom secureRandom,
+            @NonNull Encoder encoder,
+            int customerId,
+            int projectId,
+            @NonNull MetricDefinition metric,
+            @NonNull ReportDefinition report) {
+        this.mSystemData = requireNonNull(systemData);
+        this.mPrivacyGenerator = requireNonNull(privacyGenerator);
+        this.mSecureRandom = requireNonNull(secureRandom);
+        this.mEncoder = requireNonNull(encoder);
+        this.mCustomerId = customerId;
+        this.mProjectId = projectId;
+        this.mMetric = requireNonNull(metric);
+        this.mReport = requireNonNull(report);
+    }
+
+    /**
+     * Generate the private observations that for a report and day.
+     *
+     * @param dayIndex the day index to generate observations for
+     * @param allEventData the data for events that occurred that are relevant to the day and Report
+     * @return the observations to store in the DB for later sending, contained in
+     *     UnencryptedObservationBatches with their metadata
+     */
+    @Override
+    public ImmutableList<UnencryptedObservationBatch> generateObservations(
+            int dayIndex,
+            ImmutableListMultimap<SystemProfile, EventRecordAndSystemProfile> allEventData) {
+        if (allEventData.isEmpty()) {
+            return ImmutableList.of(
+                    generateObservations(
+                            dayIndex,
+                            // Use the current system profile since none is provided.
+                            mSystemData.filteredSystemProfile(mReport),
+                            ImmutableList.of()));
+        }
+
+        ImmutableList.Builder<UnencryptedObservationBatch> batches = ImmutableList.builder();
+        for (SystemProfile systemProfile : allEventData.keySet()) {
+            batches.add(
+                    generateObservations(dayIndex, systemProfile, allEventData.get(systemProfile)));
+        }
+
+        return batches.build();
+    }
+
+    /**
+     * Generate an observation batch from events for a report, day, and system profile.
+     *
+     * @param dayIndex the day observations are being generated for
+     * @param systemProfile the system profile of the observations
+     * @param events the events
+     * @return an UnencryptedObservation batch holding the generated observations
+     */
+    private UnencryptedObservationBatch generateObservations(
+            int dayIndex,
+            SystemProfile systemProfile,
+            ImmutableList<EventRecordAndSystemProfile> events) {
+        if (mReport.getEventVectorBufferMax() != 0
+                && events.size() > mReport.getEventVectorBufferMax()) {
+            // Each EventRecordAndSystemProfile contains a unique event vector for the system
+            // profile and day so the number of events can be compared to the event vector
+            // buffer max of the report.
+            events = events.subList(0, (int) mReport.getEventVectorBufferMax());
+        }
+
+        ImmutableList.Builder<Observation> observations = ImmutableList.builder();
+        for (EventRecordAndSystemProfile event : events) {
+            observations.add(
+                    Observation.newBuilder()
+                            .setPrivateIndex(
+                                    mEncoder.encode(event.eventVector(), event.aggregateValue()))
+                            .setRandomId(RandomId.generate(mSecureRandom))
+                            .build());
+        }
+        for (PrivateIndexObservation privateIndex :
+                mPrivacyGenerator.generateNoise(maxIndexForReport(), mReport)) {
+            observations.add(
+                    Observation.newBuilder()
+                            .setPrivateIndex(privateIndex)
+                            .setRandomId(RandomId.generate(mSecureRandom))
+                            .build());
+        }
+        observations.add(
+                Observation.newBuilder()
+                        .setReportParticipation(ReportParticipationObservation.getDefaultInstance())
+                        .setRandomId(RandomId.generate(mSecureRandom))
+                        .build());
+
+        ImmutableList.Builder<ObservationToEncrypt> toEncrypt = ImmutableList.builder();
+        boolean setContributionId = true;
+        for (Observation observation : observations.build()) {
+            ObservationToEncrypt.Builder builder = ObservationToEncrypt.newBuilder();
+            builder.setObservation(observation);
+            if (setContributionId) {
+                builder.setContributionId(RandomId.generate(mSecureRandom));
+            }
+
+            // Reports with privacy enabled split a single contribution across multiple
+            // observations, both private and participation. However, only 1 needs the contribution
+            // id set.
+            toEncrypt.add(builder.build());
+            setContributionId = false;
+        }
+
+        return UnencryptedObservationBatch.newBuilder()
+                .setMetadata(
+                        ObservationMetadata.newBuilder()
+                                .setCustomerId(mCustomerId)
+                                .setProjectId(mProjectId)
+                                .setMetricId(mMetric.getId())
+                                .setReportId(mReport.getId())
+                                .setDayIndex(dayIndex)
+                                .setSystemProfile(systemProfile))
+                .addAllUnencryptedObservations(toEncrypt.build())
+                .build();
+    }
+
+    private int maxIndexForReport() {
+        return PrivateIndexCalculations.getNumEventVectors(mMetric.getMetricDimensionsList())
+                        * mReport.getNumIndexPoints()
+                - 1;
+    }
+}
diff --git a/adservices/libraries/cobalt/java/com/android/cobalt/observations/RandomId.java b/adservices/libraries/cobalt/java/com/android/cobalt/observations/RandomId.java
new file mode 100644
index 0000000..e7566db
--- /dev/null
+++ b/adservices/libraries/cobalt/java/com/android/cobalt/observations/RandomId.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import com.google.protobuf.ByteString;
+
+import java.security.SecureRandom;
+
+/** Methods for generating random ids. */
+final class RandomId {
+
+    /** Generates a random id suitable use as a contribution id or an observation random id. */
+    static ByteString generate(SecureRandom secureRandom) {
+        byte[] randomId = new byte[8];
+        secureRandom.nextBytes(randomId);
+        return ByteString.copyFrom(randomId);
+    }
+
+    private RandomId() {}
+}
diff --git a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/IntegerEncoderTest.java b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/IntegerEncoderTest.java
new file mode 100644
index 0000000..dc7ba19
--- /dev/null
+++ b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/IntegerEncoderTest.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.cobalt.data.EventRecordAndSystemProfile;
+import com.android.cobalt.data.EventVector;
+import com.android.cobalt.observations.testing.FakeSecureRandom;
+
+import com.google.cobalt.AggregateValue;
+import com.google.cobalt.IntegerObservation;
+import com.google.cobalt.Observation;
+import com.google.cobalt.SystemProfile;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.security.SecureRandom;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public final class IntegerEncoderTest {
+    private static final SecureRandom SECURE_RANDOM = new FakeSecureRandom();
+    private static final SystemProfile SYSTEM_PROFILE = SystemProfile.getDefaultInstance();
+
+    @Test
+    public void encodesEventsIntoOneObservation() throws Exception {
+        EventRecordAndSystemProfile event1 =
+                EventRecordAndSystemProfile.create(
+                        SYSTEM_PROFILE,
+                        EventVector.create(ImmutableList.of(1, 5)),
+                        AggregateValue.newBuilder().setIntegerValue(100).build());
+        EventRecordAndSystemProfile event2 =
+                EventRecordAndSystemProfile.create(
+                        SYSTEM_PROFILE,
+                        EventVector.create(ImmutableList.of(2, 6)),
+                        AggregateValue.newBuilder().setIntegerValue(200).build());
+
+        IntegerObservation observation =
+                IntegerObservation.newBuilder()
+                        .addValues(
+                                IntegerObservation.Value.newBuilder()
+                                        .addAllEventCodes(List.of(1, 5))
+                                        .setValue(100))
+                        .addValues(
+                                IntegerObservation.Value.newBuilder()
+                                        .addAllEventCodes(List.of(2, 6))
+                                        .setValue(200))
+                        .build();
+
+        IntegerEncoder encoder = new IntegerEncoder(SECURE_RANDOM);
+        assertThat(encoder.encode(ImmutableList.of(event1, event2)))
+                .isEqualTo(
+                        Observation.newBuilder()
+                                .setInteger(observation)
+                                .setRandomId(
+                                        ByteString.copyFrom(new byte[] {0, 0, 0, 0, 0, 0, 0, 0}))
+                                .build());
+    }
+}
diff --git a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/NonPrivateObservationGeneratorTest.java b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/NonPrivateObservationGeneratorTest.java
new file mode 100644
index 0000000..20f0014
--- /dev/null
+++ b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/NonPrivateObservationGeneratorTest.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.cobalt.data.EventRecordAndSystemProfile;
+import com.android.cobalt.data.EventVector;
+import com.android.cobalt.observations.testing.FakeSecureRandom;
+
+import com.google.cobalt.AggregateValue;
+import com.google.cobalt.IntegerObservation;
+import com.google.cobalt.MetricDefinition;
+import com.google.cobalt.MetricDefinition.MetricType;
+import com.google.cobalt.MetricDefinition.TimeZonePolicy;
+import com.google.cobalt.Observation;
+import com.google.cobalt.ObservationMetadata;
+import com.google.cobalt.ReportDefinition;
+import com.google.cobalt.ReportDefinition.PrivacyLevel;
+import com.google.cobalt.ReportDefinition.ReportType;
+import com.google.cobalt.SystemProfile;
+import com.google.cobalt.UnencryptedObservationBatch;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.protobuf.ByteString;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.security.SecureRandom;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public final class NonPrivateObservationGeneratorTest {
+    private static final int DAY_INDEX = 19201; // 2022-07-28
+    private static final int CUSTOMER = 1;
+    private static final int PROJECT = 2;
+    private static final int METRIC_ID = 3;
+    private static final int REPORT_ID = 4;
+    private static final SystemProfile SYSTEM_PROFILE_1 =
+            SystemProfile.newBuilder().setAppVersion("1.2.3").build();
+    private static final SystemProfile SYSTEM_PROFILE_2 =
+            SystemProfile.newBuilder().setAppVersion("2.4.8").build();
+    private static final ObservationMetadata METADATA_1 =
+            ObservationMetadata.newBuilder()
+                    .setCustomerId(CUSTOMER)
+                    .setProjectId(PROJECT)
+                    .setMetricId(METRIC_ID)
+                    .setReportId(REPORT_ID)
+                    .setDayIndex(DAY_INDEX)
+                    .setSystemProfile(SYSTEM_PROFILE_1)
+                    .build();
+    private static final ObservationMetadata METADATA_2 =
+            ObservationMetadata.newBuilder()
+                    .setCustomerId(CUSTOMER)
+                    .setProjectId(PROJECT)
+                    .setMetricId(METRIC_ID)
+                    .setReportId(REPORT_ID)
+                    .setDayIndex(DAY_INDEX)
+                    .setSystemProfile(SYSTEM_PROFILE_2)
+                    .build();
+    private static final int EVENT_COUNT_1 = 3;
+    private static final int EVENT_COUNT_2 = 17;
+    private static final EventVector EVENT_VECTOR_1 = EventVector.create(ImmutableList.of(1, 5));
+    private static final EventVector EVENT_VECTOR_2 = EventVector.create(ImmutableList.of(2, 6));
+    private static final EventRecordAndSystemProfile EVENT_1 =
+            createEvent(EVENT_VECTOR_1, EVENT_COUNT_1);
+    private static final EventRecordAndSystemProfile EVENT_2 =
+            createEvent(EVENT_VECTOR_2, EVENT_COUNT_2);
+
+    // Deterministic randomly generated bytes due to the FakeSecureRandom.
+    private static final ByteString RANDOM_BYTES_1 =
+            ByteString.copyFrom(new byte[] {0, 0, 0, 0, 0, 0, 0, 0});
+    private static final ByteString RANDOM_BYTES_2 =
+            ByteString.copyFrom(new byte[] {1, 1, 1, 1, 1, 1, 1, 1});
+    private static final ByteString RANDOM_BYTES_3 =
+            ByteString.copyFrom(new byte[] {2, 2, 2, 2, 2, 2, 2, 2});
+    private static final ByteString RANDOM_BYTES_4 =
+            ByteString.copyFrom(new byte[] {3, 3, 3, 3, 3, 3, 3, 3});
+    private static final Observation OBSERVATION_1 =
+            Observation.newBuilder()
+                    .setInteger(
+                            IntegerObservation.newBuilder()
+                                    .addValues(
+                                            IntegerObservation.Value.newBuilder()
+                                                    .setValue(EVENT_COUNT_1)
+                                                    .addAllEventCodes(EVENT_VECTOR_1.eventCodes())))
+                    .setRandomId(RANDOM_BYTES_1)
+                    .build();
+    private static final Observation OBSERVATION_1_AND_2 =
+            Observation.newBuilder()
+                    .setInteger(
+                            IntegerObservation.newBuilder()
+                                    .addValues(
+                                            IntegerObservation.Value.newBuilder()
+                                                    .setValue(EVENT_COUNT_1)
+                                                    .addAllEventCodes(EVENT_VECTOR_1.eventCodes()))
+                                    .addValues(
+                                            IntegerObservation.Value.newBuilder()
+                                                    .setValue(EVENT_COUNT_2)
+                                                    .addAllEventCodes(EVENT_VECTOR_2.eventCodes())))
+                    .setRandomId(RANDOM_BYTES_1)
+                    .build();
+    private static final Observation OBSERVATION_2 =
+            Observation.newBuilder()
+                    .setInteger(
+                            IntegerObservation.newBuilder()
+                                    .addValues(
+                                            IntegerObservation.Value.newBuilder()
+                                                    .setValue(EVENT_COUNT_2)
+                                                    .addAllEventCodes(EVENT_VECTOR_2.eventCodes())))
+                    .setRandomId(RANDOM_BYTES_3)
+                    .build();
+    private static final Observation NO_EVENT_CODES_OBSERVATION =
+            Observation.newBuilder()
+                    .setInteger(
+                            IntegerObservation.newBuilder()
+                                    .addValues(IntegerObservation.Value.newBuilder().setValue(7)))
+                    .setRandomId(RANDOM_BYTES_1)
+                    .build();
+
+    private static final MetricDefinition METRIC =
+            MetricDefinition.newBuilder()
+                    .setId(METRIC_ID)
+                    .setMetricType(MetricType.OCCURRENCE)
+                    .setTimeZonePolicy(TimeZonePolicy.OTHER_TIME_ZONE)
+                    .setOtherTimeZone("America/Los_Angeles")
+                    .build();
+    private static final ReportDefinition REPORT =
+            ReportDefinition.newBuilder()
+                    .setId(REPORT_ID)
+                    .setReportType(ReportType.FLEETWIDE_OCCURRENCE_COUNTS)
+                    .setPrivacyLevel(PrivacyLevel.NO_ADDED_PRIVACY)
+                    .build();
+
+    private final SecureRandom mSecureRandom;
+    private NonPrivateObservationGenerator mGenerator;
+
+    public NonPrivateObservationGeneratorTest() {
+        mSecureRandom = new FakeSecureRandom();
+        mGenerator = null;
+    }
+
+    private NonPrivateObservationGenerator createObservationGenerator(
+            int customerId, int projectId, MetricDefinition metric, ReportDefinition report) {
+        return new NonPrivateObservationGenerator(
+                mSecureRandom,
+                new IntegerEncoder(mSecureRandom),
+                customerId,
+                projectId,
+                metric.getId(),
+                report);
+    }
+
+    private static EventRecordAndSystemProfile createEvent(
+            List<Integer> eventCodes, int aggregateValue) {
+        // System profile fields are ignored during observation generation and can be anything.
+        return EventRecordAndSystemProfile.create(
+                /* systemProfile= */ SystemProfile.getDefaultInstance(),
+                EventVector.create(eventCodes),
+                AggregateValue.newBuilder().setIntegerValue(aggregateValue).build());
+    }
+
+    private static EventRecordAndSystemProfile createEvent(
+            EventVector eventVector, int aggregateValue) {
+        // System profile fields are ignored during observation generation and can be anything.
+        return EventRecordAndSystemProfile.create(
+                /* systemProfile= */ SystemProfile.getDefaultInstance(),
+                eventVector,
+                AggregateValue.newBuilder().setIntegerValue(aggregateValue).build());
+    }
+
+    @Test
+    public void generateObservations_noEvents_nothingGenerated() throws Exception {
+        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
+        List<UnencryptedObservationBatch> result =
+                mGenerator.generateObservations(DAY_INDEX, ImmutableListMultimap.of());
+        assertThat(result).isEmpty();
+    }
+
+    @Test
+    public void generateObservations_oneEvent_generated() throws Exception {
+        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
+        List<UnencryptedObservationBatch> result =
+                mGenerator.generateObservations(
+                        DAY_INDEX, ImmutableListMultimap.of(SYSTEM_PROFILE_1, EVENT_1));
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
+        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
+        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
+                .isEqualTo(RANDOM_BYTES_2);
+        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
+                .isEqualTo(OBSERVATION_1);
+    }
+
+    @Test
+    public void generateObservations_oneEventWithNoEventCodes_generated() throws Exception {
+        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
+        List<UnencryptedObservationBatch> result =
+                mGenerator.generateObservations(
+                        DAY_INDEX,
+                        ImmutableListMultimap.of(
+                                SYSTEM_PROFILE_1, createEvent(ImmutableList.of(), 7)));
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
+        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
+        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
+                .isEqualTo(RANDOM_BYTES_2);
+        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
+                .isEqualTo(NO_EVENT_CODES_OBSERVATION);
+    }
+
+    @Test
+    public void generateObservations_twoEvents_oneObservationGenerated() throws Exception {
+        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
+        List<UnencryptedObservationBatch> result =
+                mGenerator.generateObservations(
+                        DAY_INDEX,
+                        ImmutableListMultimap.of(
+                                SYSTEM_PROFILE_1, EVENT_1, SYSTEM_PROFILE_1, EVENT_2));
+
+        // Verify both event vectors are aggregated into one observation.
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
+        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
+        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
+                .isEqualTo(RANDOM_BYTES_2);
+        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
+                .isEqualTo(OBSERVATION_1_AND_2);
+    }
+
+    @Test
+    public void generateObservations_twoEventsInTwoSystemProfiles_separateObservations()
+            throws Exception {
+        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
+        List<UnencryptedObservationBatch> result =
+                mGenerator.generateObservations(
+                        DAY_INDEX,
+                        ImmutableListMultimap.of(
+                                SYSTEM_PROFILE_1, EVENT_1, SYSTEM_PROFILE_2, EVENT_2));
+
+        // Verify that separate system profiles are aggregated into separate batches.
+        assertThat(result).hasSize(2);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
+        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
+        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
+                .isEqualTo(RANDOM_BYTES_2);
+        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
+                .isEqualTo(OBSERVATION_1);
+        assertThat(result.get(1).getMetadata()).isEqualTo(METADATA_2);
+        assertThat(result.get(1).getUnencryptedObservationsList()).hasSize(1);
+        assertThat(result.get(1).getUnencryptedObservations(0).getContributionId())
+                .isEqualTo(RANDOM_BYTES_4);
+        assertThat(result.get(1).getUnencryptedObservations(0).getObservation())
+                .isEqualTo(OBSERVATION_2);
+    }
+
+    @Test
+    public void generateObservations_eventVectorBufferMax_oneEventSent() throws Exception {
+        mGenerator =
+                createObservationGenerator(
+                        CUSTOMER,
+                        PROJECT,
+                        METRIC,
+                        REPORT.toBuilder().setEventVectorBufferMax(1).build());
+        List<UnencryptedObservationBatch> result =
+                mGenerator.generateObservations(
+                        DAY_INDEX,
+                        ImmutableListMultimap.of(
+                                SYSTEM_PROFILE_1, EVENT_1, SYSTEM_PROFILE_1, EVENT_2));
+
+        // Verify only the first event vector is aggregated into an observation.
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
+        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
+        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
+                .isEqualTo(RANDOM_BYTES_2);
+        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
+                .isEqualTo(OBSERVATION_1);
+    }
+}
diff --git a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/ObservationGeneratorFactoryTest.java b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/ObservationGeneratorFactoryTest.java
new file mode 100644
index 0000000..be0d6c5
--- /dev/null
+++ b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/ObservationGeneratorFactoryTest.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import com.android.cobalt.data.ObservationGenerator;
+import com.android.cobalt.domain.Project;
+import com.android.cobalt.observations.testing.FakeSecureRandom;
+import com.android.cobalt.system.SystemData;
+
+import com.google.cobalt.MetricDefinition;
+import com.google.cobalt.ReportDefinition;
+import com.google.cobalt.ReportDefinition.PrivacyLevel;
+import com.google.cobalt.ReportDefinition.ReportType;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.security.SecureRandom;
+import java.util.List;
+
+@RunWith(JUnit4.class)
+public final class ObservationGeneratorFactoryTest {
+    private final ObservationGeneratorFactory mFactory;
+
+    public ObservationGeneratorFactoryTest() {
+        Project project =
+                Project.create(/* customerId= */ 0, /* projectId= */ 1, /* metrics= */ List.of());
+        SecureRandom secureRandom = new FakeSecureRandom();
+        this.mFactory =
+                new ObservationGeneratorFactory(
+                        project,
+                        new SystemData(),
+                        new PrivacyGenerator(secureRandom),
+                        secureRandom);
+    }
+
+    @Test
+    public void getObservationGenerator_nonPrivateFleetwideOccurrenceCounts() throws Exception {
+        ObservationGenerator generator =
+                mFactory.getObservationGenerator(
+                        MetricDefinition.getDefaultInstance(),
+                        ReportDefinition.newBuilder()
+                                .setReportType(ReportType.FLEETWIDE_OCCURRENCE_COUNTS)
+                                .setPrivacyLevel(PrivacyLevel.NO_ADDED_PRIVACY)
+                                .build());
+
+        assertThat(generator).isInstanceOf(NonPrivateObservationGenerator.class);
+    }
+
+    @Test
+    public void getObservationGenerator_privateFleetwideOccurrenceCounts() throws Exception {
+        ObservationGenerator generator =
+                mFactory.getObservationGenerator(
+                        MetricDefinition.getDefaultInstance(),
+                        ReportDefinition.newBuilder()
+                                .setReportType(ReportType.FLEETWIDE_OCCURRENCE_COUNTS)
+                                .setPrivacyLevel(PrivacyLevel.HIGH_PRIVACY)
+                                .build());
+
+        assertThat(generator).isInstanceOf(PrivateObservationGenerator.class);
+    }
+
+    @Test
+    public void getObservationGenerator_fleetwideOccurrenceCounts_noPrivacyLevelSet()
+            throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.FLEETWIDE_OCCURRENCE_COUNTS)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_reportTypeNotSet_throwsAssertionError() throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder().build()));
+    }
+
+    @Test
+    public void getObservationGenerator_reportTypeUnset_throwsAssertionError() throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.REPORT_TYPE_UNSET)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_uniqueDeviceCounts_throwsAssertionError() throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.UNIQUE_DEVICE_COUNTS)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_uniqueDeviceHistograms_throwsAssertionError()
+            throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.UNIQUE_DEVICE_HISTOGRAMS)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_hourlyValueHistograms_throwsAssertionError()
+            throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.HOURLY_VALUE_HISTOGRAMS)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_fleetwideHistograms_throwsAssertionError()
+            throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.FLEETWIDE_HISTOGRAMS)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_fleetwideMeans_throwsAssertionError() throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.FLEETWIDE_MEANS)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_uniqueDeviceNumericStats_throwsAssertionError()
+            throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.UNIQUE_DEVICE_NUMERIC_STATS)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_hourlyValueNumericStats_throwsAssertionError()
+            throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.HOURLY_VALUE_NUMERIC_STATS)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_stringsCounts_throwsAssertionError() throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.STRING_COUNTS)
+                                        .build()));
+    }
+
+    @Test
+    public void getObservationGenerator_uniqueDeviceStringCounts_throwsAssertionError()
+            throws Exception {
+        assertThrows(
+                AssertionError.class,
+                () ->
+                        mFactory.getObservationGenerator(
+                                MetricDefinition.getDefaultInstance(),
+                                ReportDefinition.newBuilder()
+                                        .setReportType(ReportType.UNIQUE_DEVICE_STRING_COUNTS)
+                                        .build()));
+    }
+}
diff --git a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivacyGeneratorStatisticalTest.java b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivacyGeneratorStatisticalTest.java
index 4df99ab..f85d35a 100644
--- a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivacyGeneratorStatisticalTest.java
+++ b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivacyGeneratorStatisticalTest.java
@@ -21,6 +21,7 @@
 
 import androidx.test.runner.AndroidJUnit4;
 
+import com.google.cobalt.PrivateIndexObservation;
 import com.google.cobalt.ReportDefinition;
 import com.google.cobalt.ReportDefinition.PrivacyLevel;
 import com.google.common.collect.ImmutableList;
@@ -46,7 +47,6 @@
                     .setId(5)
                     .setPrivacyLevel(PrivacyLevel.LOW_PRIVACY)
                     .build();
-    private static final ImmutableList<Integer> sEmptyIndices = ImmutableList.of();
 
     private final SecureRandom mSecureRandom;
     private final PrivacyGenerator mPrivacyGenerator;
@@ -99,7 +99,7 @@
         ReportDefinition report = sReportTemplate.toBuilder().setPoissonMean(poissonMean).build();
         int totalIndicesAdded = 0;
         for (int i = 0; i < numTrials; i++) {
-            totalIndicesAdded += mPrivacyGenerator.addNoise(sEmptyIndices, maxIndex, report).size();
+            totalIndicesAdded += mPrivacyGenerator.generateNoise(maxIndex, report).size();
         }
         double stddev = Math.sqrt((double) (numTrials * (1 + maxIndex)) * poissonMean);
         double expectedIndicesAdded = (double) (numTrials * (1 + maxIndex)) * poissonMean;
@@ -231,11 +231,11 @@
         int numIndicesAdded = 0;
         int[] indexCounts = new int[n];
         while (numIndicesAdded < numTrials) {
-            ImmutableList<Integer> noisedIndices =
-                    mPrivacyGenerator.addNoise(sEmptyIndices, maxIndex, report);
-            for (int i : noisedIndices) {
+            ImmutableList<PrivateIndexObservation> noisedIndices =
+                    mPrivacyGenerator.generateNoise(maxIndex, report);
+            for (PrivateIndexObservation o : noisedIndices) {
                 numIndicesAdded++;
-                indexCounts[i]++;
+                indexCounts[(int) o.getIndex()]++;
                 if (numIndicesAdded >= numTrials) {
                     break;
                 }
diff --git a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivacyGeneratorTest.java b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivacyGeneratorTest.java
index e4e4d22..cf455d3 100644
--- a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivacyGeneratorTest.java
+++ b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivacyGeneratorTest.java
@@ -24,6 +24,7 @@
 
 import com.android.cobalt.observations.testing.FakeSecureRandom;
 
+import com.google.cobalt.PrivateIndexObservation;
 import com.google.cobalt.ReportDefinition;
 import com.google.cobalt.ReportDefinition.PrivacyLevel;
 import com.google.common.collect.ImmutableList;
@@ -46,78 +47,70 @@
         mPrivacyGenerator = new PrivacyGenerator(new FakeSecureRandom());
     }
 
+    private static PrivateIndexObservation makeObservation(int i) {
+        return PrivateIndexObservation.newBuilder().setIndex(i).build();
+    }
+
     @Test
-    public void testAddNoise_noEventsNoNoise_empty() throws Exception {
-        ImmutableList<Integer> result = mPrivacyGenerator.addNoise(ImmutableList.of(), 0, sReport);
+    public void testAddNoise_noNoise_empty() throws Exception {
+        ImmutableList<PrivateIndexObservation> result = mPrivacyGenerator.generateNoise(0, sReport);
         // The report's lambda is too small to trigger a fabricated observation.
         assertThat(result).isEmpty();
     }
 
     @Test
-    public void testAddNoise_noEventsButFabricatedObservation_oneIndex() throws Exception {
+    public void testAddNoise_fabricatedObservation_oneIndex() throws Exception {
         // Use a larger Poisson mean that is guaranteed to cause a fabricated observation to be
         // created, due to the FakeSecureRandom implementation.
-        ImmutableList<Integer> result =
-                mPrivacyGenerator.addNoise(
-                        ImmutableList.of(), 0, sReport.toBuilder().setPoissonMean(0.1).build());
+        ImmutableList<PrivateIndexObservation> result =
+                mPrivacyGenerator.generateNoise(0, sReport.toBuilder().setPoissonMean(0.1).build());
         // A fabricated observation.
-        assertThat(result).containsExactly(0);
+        assertThat(result).containsExactly(makeObservation(0));
     }
 
     @Test
-    public void testAddNoise_noEventsButTwoFabricatedObservations_oneIndex() throws Exception {
+    public void testAddNoise_twoFabricatedObservations_oneIndex() throws Exception {
         // Use an even larger Poisson mean that is guaranteed to cause two fabricated observations
         // to be created, due to the FakeSecureRandom implementation.
-        ImmutableList<Integer> result =
-                mPrivacyGenerator.addNoise(
-                        ImmutableList.of(), 0, sReport.toBuilder().setPoissonMean(0.52).build());
+        ImmutableList<PrivateIndexObservation> result =
+                mPrivacyGenerator.generateNoise(
+                        0, sReport.toBuilder().setPoissonMean(0.52).build());
         // Two fabricated observations.
-        assertThat(result).containsExactly(0, 0);
+        assertThat(result).containsExactly(makeObservation(0), makeObservation(0));
     }
 
     @Test
-    public void testAddNoise_oneEventNoNoise_oneIndex() throws Exception {
-        ImmutableList<Integer> result = mPrivacyGenerator.addNoise(ImmutableList.of(0), 0, sReport);
-        // Real index returned, as the report's lambda is too small to trigger a fabricated
-        // observation.
-        assertThat(result).containsExactly(0);
-    }
-
-    @Test
-    public void testAddNoise_oneEventAndFabricatedObservation_twoIndices() throws Exception {
+    public void testAddNoise_oneFabricatedObservation_twoIndices() throws Exception {
         // Use a larger Poisson mean that is guaranteed to cause a fabricated observation to be
         // created, due to the FakeSecureRandom implementation.
-        ImmutableList<Integer> result =
-                mPrivacyGenerator.addNoise(
-                        ImmutableList.of(0), 0, sReport.toBuilder().setPoissonMean(0.1).build());
-        // Real index returned, and a fabricated index are expected.
-        assertThat(result).containsExactly(0, 0);
+        ImmutableList<PrivateIndexObservation> result =
+                mPrivacyGenerator.generateNoise(0, sReport.toBuilder().setPoissonMean(0.1).build());
+        // A fabricated index is expected.
+        assertThat(result).containsExactly(makeObservation(0));
     }
 
     @Test
-    public void testAddNoise_oneEventAndTwoFabricatedObservations_threeIndices() throws Exception {
+    public void testAddNoise_twoFabricatedObservations_threeIndices() throws Exception {
         // Use an even larger Poisson mean that is guaranteed to cause two fabricated observations
         // to be created, due to the FakeSecureRandom implementation.
-        ImmutableList<Integer> result =
-                mPrivacyGenerator.addNoise(
-                        ImmutableList.of(0), 0, sReport.toBuilder().setPoissonMean(0.52).build());
-        // Real index returned, and two fabricated indices are expected.
-        assertThat(result).containsExactly(0, 0, 0);
+        ImmutableList<PrivateIndexObservation> result =
+                mPrivacyGenerator.generateNoise(
+                        0, sReport.toBuilder().setPoissonMean(0.52).build());
+        // Two fabricated indices are expected.
+        assertThat(result).containsExactly(makeObservation(0), makeObservation(0));
     }
 
     @Test
-    public void testAddNoise_oneEventForMetricWithDimensions_threeObservations() throws Exception {
+    public void testAddNoise_metricWithDimensions_threeObservations() throws Exception {
         // Use a larger Poisson mean that is guaranteed to cause a single fabricated observation to
         // be created, due to the FakeSecureRandom implementation. This is smaller than other tests,
         // because the poisson mean is multiplied by the number of indices, which is larger here due
         // the metric dimensions.
-        ImmutableList<Integer> result =
-                mPrivacyGenerator.addNoise(
-                        ImmutableList.of(2, 3),
-                        5,
-                        sReport.toBuilder().setPoissonMean(0.02).build());
-        // Real indices returned, and a fabricated index are expected.
-        assertThat(result).containsExactly(2, 3, 5);
+        ImmutableList<PrivateIndexObservation> result =
+                mPrivacyGenerator.generateNoise(
+                        5, sReport.toBuilder().setPoissonMean(0.02).build());
+        // A fabricated index is expected.
+        assertThat(result).containsExactly(makeObservation(5));
     }
 
     @Test
@@ -125,7 +118,7 @@
         IllegalArgumentException thrown =
                 assertThrows(
                         IllegalArgumentException.class,
-                        () -> mPrivacyGenerator.addNoise(ImmutableList.of(), -1, sReport));
+                        () -> mPrivacyGenerator.generateNoise(-1, sReport));
         assertThat(thrown).hasMessageThat().contains("maxIndex value cannot be negative");
     }
 
@@ -135,10 +128,8 @@
                 assertThrows(
                         IllegalArgumentException.class,
                         () ->
-                                mPrivacyGenerator.addNoise(
-                                        ImmutableList.of(),
-                                        0,
-                                        sReport.toBuilder().setPoissonMean(-0.1).build()));
+                                mPrivacyGenerator.generateNoise(
+                                        0, sReport.toBuilder().setPoissonMean(-0.1).build()));
         assertThat(thrown).hasMessageThat().contains("poisson_mean must be positive");
     }
 }
diff --git a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivateIntegerEncoderTest.java b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivateIntegerEncoderTest.java
new file mode 100644
index 0000000..158b0db
--- /dev/null
+++ b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivateIntegerEncoderTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 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.cobalt.observations;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.cobalt.data.EventVector;
+import com.android.cobalt.observations.testing.FakeSecureRandom;
+
+import com.google.cobalt.AggregateValue;
+import com.google.cobalt.MetricDefinition;
+import com.google.cobalt.MetricDefinition.MetricDimension;
+import com.google.cobalt.PrivateIndexObservation;
+import com.google.cobalt.ReportDefinition;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.security.SecureRandom;
+
+@RunWith(JUnit4.class)
+public final class PrivateIntegerEncoderTest {
+    private static final SecureRandom SECURE_RANDOM = new FakeSecureRandom();
+
+    @Test
+    public void encodesAsPrivateIndex() throws Exception {
+        // Use a metric with 6 possible event vectors: [(0,5), (1,5), (2,5), (0,6), (1,6), (2,6)].
+        MetricDefinition metric =
+                MetricDefinition.newBuilder()
+                        .addMetricDimensions(MetricDimension.newBuilder().setMaxEventCode(2))
+                        .addMetricDimensions(
+                                MetricDimension.newBuilder()
+                                        .putEventCodes(5, "5")
+                                        .putEventCodes(6, "6"))
+                        .build();
+
+        // Use a report with 21 possible values and 11 index points so private index encoding of a
+        // value `v` is always `6 * floor(v/2) + eventIndex` where `eventIndex` is the index of the
+        // event vector in the above list.
+        ReportDefinition report =
+                ReportDefinition.newBuilder()
+                        .setNumIndexPoints(11)
+                        .setMinValue(0)
+                        .setMaxValue(20)
+                        .build();
+        PrivateIntegerEncoder encoder = new PrivateIntegerEncoder(SECURE_RANDOM, metric, report);
+        assertThat(
+                        encoder.encode(
+                                EventVector.create(1, 5),
+                                AggregateValue.newBuilder().setIntegerValue(3).build()))
+                .isEqualTo(PrivateIndexObservation.newBuilder().setIndex(7).build());
+        assertThat(
+                        encoder.encode(
+                                EventVector.create(2, 6),
+                                AggregateValue.newBuilder().setIntegerValue(17).build()))
+                .isEqualTo(PrivateIndexObservation.newBuilder().setIndex(53).build());
+    }
+
+    @Test
+    public void valueBelowMinimum_encodedAsMinimumValue() throws Exception {
+        // Use a metric with 6 possible event vectors: [(0,5), (1,5), (2,5), (0,6), (1,6), (2,6)].
+        MetricDefinition metric =
+                MetricDefinition.newBuilder()
+                        .addMetricDimensions(MetricDimension.newBuilder().setMaxEventCode(2))
+                        .addMetricDimensions(
+                                MetricDimension.newBuilder()
+                                        .putEventCodes(5, "5")
+                                        .putEventCodes(6, "6"))
+                        .build();
+
+        // Use a report with 21 possible values and 11 index points so private index encoding of a
+        // value `v` is always `6 * floor(v/2) + eventIndex` where `eventIndex` is the index of the
+        // event vector in the above list.
+        ReportDefinition report =
+                ReportDefinition.newBuilder()
+                        .setNumIndexPoints(11)
+                        .setMinValue(0)
+                        .setMaxValue(20)
+                        .build();
+        PrivateIntegerEncoder encoder = new PrivateIntegerEncoder(SECURE_RANDOM, metric, report);
+        assertThat(
+                        encoder.encode(
+                                EventVector.create(1, 5),
+                                AggregateValue.newBuilder().setIntegerValue(-1).build()))
+                .isEqualTo(
+                        encoder.encode(
+                                EventVector.create(1, 5),
+                                AggregateValue.newBuilder().setIntegerValue(0).build()));
+    }
+
+    @Test
+    public void valueAboveMaximum_encodedAsMaximumValue() throws Exception {
+        // Use a metric with 6 possible event vectors: [(0,5), (1,5), (2,5), (0,6), (1,6), (2,6)].
+        MetricDefinition metric =
+                MetricDefinition.newBuilder()
+                        .addMetricDimensions(MetricDimension.newBuilder().setMaxEventCode(2))
+                        .addMetricDimensions(
+                                MetricDimension.newBuilder()
+                                        .putEventCodes(5, "5")
+                                        .putEventCodes(6, "6"))
+                        .build();
+
+        // Use a report with 21 possible values and 11 index points so private index encoding of a
+        // value `v` is always `6 * floor(v/2) + eventIndex` where `eventIndex` is the index of the
+        // event vector in the above list.
+        ReportDefinition report =
+                ReportDefinition.newBuilder()
+                        .setNumIndexPoints(11)
+                        .setMinValue(0)
+                        .setMaxValue(20)
+                        .build();
+        PrivateIntegerEncoder encoder = new PrivateIntegerEncoder(SECURE_RANDOM, metric, report);
+        assertThat(
+                        encoder.encode(
+                                EventVector.create(1, 5),
+                                AggregateValue.newBuilder().setIntegerValue(21).build()))
+                .isEqualTo(
+                        encoder.encode(
+                                EventVector.create(1, 5),
+                                AggregateValue.newBuilder().setIntegerValue(20).build()));
+    }
+}
diff --git a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/CountObservationGeneratorTest.java b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivateObservationGeneratorTest.java
similarity index 64%
rename from adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/CountObservationGeneratorTest.java
rename to adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivateObservationGeneratorTest.java
index 78df537..b28fa99 100644
--- a/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/CountObservationGeneratorTest.java
+++ b/adservices/libraries/cobalt/tests/src/com/android/cobalt/observations/PrivateObservationGeneratorTest.java
@@ -26,7 +26,6 @@
 import com.android.cobalt.system.SystemData;
 
 import com.google.cobalt.AggregateValue;
-import com.google.cobalt.IntegerObservation;
 import com.google.cobalt.MetricDefinition;
 import com.google.cobalt.MetricDefinition.MetricDimension;
 import com.google.cobalt.MetricDefinition.MetricType;
@@ -52,13 +51,12 @@
 import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
-public final class CountObservationGeneratorTest {
+public final class PrivateObservationGeneratorTest {
     private static final int DAY_INDEX = 19201; // 2022-07-28
     private static final int CUSTOMER = 1;
     private static final int PROJECT = 2;
     private static final int METRIC_ID = 3;
     private static final int REPORT_ID = 4;
-    private static final int PRIVATE_REPORT_ID = 5;
     private static final SystemProfile SYSTEM_PROFILE_1 =
             SystemProfile.newBuilder().setAppVersion("1.2.3").build();
     private static final SystemProfile SYSTEM_PROFILE_2 =
@@ -81,32 +79,12 @@
                     .setDayIndex(DAY_INDEX)
                     .setSystemProfile(SYSTEM_PROFILE_2)
                     .build();
-    private static final ObservationMetadata PRIVATE_METADATA_1 =
-            ObservationMetadata.newBuilder()
-                    .setCustomerId(CUSTOMER)
-                    .setProjectId(PROJECT)
-                    .setMetricId(METRIC_ID)
-                    .setReportId(PRIVATE_REPORT_ID)
-                    .setDayIndex(DAY_INDEX)
-                    .setSystemProfile(SYSTEM_PROFILE_1)
-                    .build();
-    private static final ObservationMetadata PRIVATE_METADATA_2 =
-            ObservationMetadata.newBuilder()
-                    .setCustomerId(CUSTOMER)
-                    .setProjectId(PROJECT)
-                    .setMetricId(METRIC_ID)
-                    .setReportId(PRIVATE_REPORT_ID)
-                    .setDayIndex(DAY_INDEX)
-                    .setSystemProfile(SYSTEM_PROFILE_2)
-                    .build();
     private static final int EVENT_COUNT_1 = 3;
     private static final int EVENT_COUNT_2 = 17;
     private static final EventVector EVENT_VECTOR_1 = EventVector.create(ImmutableList.of(1, 5));
     private static final EventVector EVENT_VECTOR_2 = EventVector.create(ImmutableList.of(2, 6));
     private static final EventRecordAndSystemProfile EVENT_1 =
             createEvent(EVENT_VECTOR_1, EVENT_COUNT_1);
-    private static final EventRecordAndSystemProfile EVENT_2 =
-            createEvent(EVENT_VECTOR_2, EVENT_COUNT_2);
 
     // Deterministic randomly generated bytes due to the FakeSecureRandom.
     private static final ByteString RANDOM_BYTES_1 =
@@ -117,47 +95,6 @@
             ByteString.copyFrom(new byte[] {2, 2, 2, 2, 2, 2, 2, 2});
     private static final ByteString RANDOM_BYTES_4 =
             ByteString.copyFrom(new byte[] {3, 3, 3, 3, 3, 3, 3, 3});
-    private static final Observation OBSERVATION_1 =
-            Observation.newBuilder()
-                    .setInteger(
-                            IntegerObservation.newBuilder()
-                                    .addValues(
-                                            IntegerObservation.Value.newBuilder()
-                                                    .setValue(EVENT_COUNT_1)
-                                                    .addAllEventCodes(EVENT_VECTOR_1.eventCodes())))
-                    .setRandomId(RANDOM_BYTES_1)
-                    .build();
-    private static final Observation OBSERVATION_1_AND_2 =
-            Observation.newBuilder()
-                    .setInteger(
-                            IntegerObservation.newBuilder()
-                                    .addValues(
-                                            IntegerObservation.Value.newBuilder()
-                                                    .setValue(EVENT_COUNT_1)
-                                                    .addAllEventCodes(EVENT_VECTOR_1.eventCodes()))
-                                    .addValues(
-                                            IntegerObservation.Value.newBuilder()
-                                                    .setValue(EVENT_COUNT_2)
-                                                    .addAllEventCodes(EVENT_VECTOR_2.eventCodes())))
-                    .setRandomId(RANDOM_BYTES_1)
-                    .build();
-    private static final Observation OBSERVATION_2 =
-            Observation.newBuilder()
-                    .setInteger(
-                            IntegerObservation.newBuilder()
-                                    .addValues(
-                                            IntegerObservation.Value.newBuilder()
-                                                    .setValue(EVENT_COUNT_2)
-                                                    .addAllEventCodes(EVENT_VECTOR_2.eventCodes())))
-                    .setRandomId(RANDOM_BYTES_3)
-                    .build();
-    private static final Observation NO_EVENT_CODES_OBSERVATION =
-            Observation.newBuilder()
-                    .setInteger(
-                            IntegerObservation.newBuilder()
-                                    .addValues(IntegerObservation.Value.newBuilder().setValue(7)))
-                    .setRandomId(RANDOM_BYTES_1)
-                    .build();
 
     private static final MetricDefinition METRIC =
             MetricDefinition.newBuilder()
@@ -182,12 +119,6 @@
             ReportDefinition.newBuilder()
                     .setId(REPORT_ID)
                     .setReportType(ReportType.FLEETWIDE_OCCURRENCE_COUNTS)
-                    .setPrivacyLevel(PrivacyLevel.NO_ADDED_PRIVACY)
-                    .build();
-    private static final ReportDefinition PRIVATE_REPORT =
-            ReportDefinition.newBuilder()
-                    .setId(PRIVATE_REPORT_ID)
-                    .setReportType(ReportType.FLEETWIDE_OCCURRENCE_COUNTS)
                     .addSystemProfileField(SystemProfileField.APP_VERSION)
                     .setPrivacyLevel(PrivacyLevel.LOW_PRIVACY)
                     // Use a poisson mean that will not produce a fabricated observation.
@@ -205,20 +136,21 @@
 
     private final SecureRandom mSecureRandom;
     private final PrivacyGenerator mPrivacyGenerator;
-    private CountObservationGenerator mGenerator;
+    private PrivateObservationGenerator mGenerator;
 
-    public CountObservationGeneratorTest() {
+    public PrivateObservationGeneratorTest() {
         mSecureRandom = new FakeSecureRandom();
         mPrivacyGenerator = new PrivacyGenerator(mSecureRandom);
         mGenerator = null;
     }
 
-    private CountObservationGenerator createObservationGenerator(
+    private PrivateObservationGenerator createObservationGenerator(
             int customerId, int projectId, MetricDefinition metric, ReportDefinition report) {
-        return new CountObservationGenerator(
+        return new PrivateObservationGenerator(
                 new SystemData(SYSTEM_PROFILE_1.getAppVersion()),
                 mPrivacyGenerator,
                 mSecureRandom,
+                new PrivateIntegerEncoder(mSecureRandom, metric, report),
                 customerId,
                 projectId,
                 metric,
@@ -244,125 +176,16 @@
     }
 
     @Test
-    public void generateObservations_noEvents_nothingGenerated() throws Exception {
+    public void generateObservations_noEvents_reportParticipationOnly() throws Exception {
         mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
         List<UnencryptedObservationBatch> result =
                 mGenerator.generateObservations(DAY_INDEX, ImmutableListMultimap.of());
-        assertThat(result).isEmpty();
-    }
-
-    @Test
-    public void generateObservations_oneEvent_generated() throws Exception {
-        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
-        List<UnencryptedObservationBatch> result =
-                mGenerator.generateObservations(
-                        DAY_INDEX, ImmutableListMultimap.of(SYSTEM_PROFILE_1, EVENT_1));
-        assertThat(result).hasSize(1);
-        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
-        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
-        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
-                .isEqualTo(RANDOM_BYTES_2);
-        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
-                .isEqualTo(OBSERVATION_1);
-    }
-
-    @Test
-    public void generateObservations_oneEventWithNoEventCodes_generated() throws Exception {
-        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
-        List<UnencryptedObservationBatch> result =
-                mGenerator.generateObservations(
-                        DAY_INDEX,
-                        ImmutableListMultimap.of(
-                                SYSTEM_PROFILE_1, createEvent(ImmutableList.of(), 7)));
-        assertThat(result).hasSize(1);
-        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
-        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
-        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
-                .isEqualTo(RANDOM_BYTES_2);
-        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
-                .isEqualTo(NO_EVENT_CODES_OBSERVATION);
-    }
-
-    @Test
-    public void generateObservations_twoEvents_oneObservationGenerated() throws Exception {
-        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
-        List<UnencryptedObservationBatch> result =
-                mGenerator.generateObservations(
-                        DAY_INDEX,
-                        ImmutableListMultimap.of(
-                                SYSTEM_PROFILE_1, EVENT_1, SYSTEM_PROFILE_1, EVENT_2));
-
-        // Verify both event vectors are aggregated into one observation.
-        assertThat(result).hasSize(1);
-        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
-        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
-        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
-                .isEqualTo(RANDOM_BYTES_2);
-        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
-                .isEqualTo(OBSERVATION_1_AND_2);
-    }
-
-    @Test
-    public void generateObservations_twoEventsInTwoSystemProfiles_separateObservations()
-            throws Exception {
-        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
-        List<UnencryptedObservationBatch> result =
-                mGenerator.generateObservations(
-                        DAY_INDEX,
-                        ImmutableListMultimap.of(
-                                SYSTEM_PROFILE_1, EVENT_1, SYSTEM_PROFILE_2, EVENT_2));
-
-        // Verify that separate system profiles are aggregated into separate batches.
-        assertThat(result).hasSize(2);
-        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
-        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
-        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
-                .isEqualTo(RANDOM_BYTES_2);
-        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
-                .isEqualTo(OBSERVATION_1);
-        assertThat(result.get(1).getMetadata()).isEqualTo(METADATA_2);
-        assertThat(result.get(1).getUnencryptedObservationsList()).hasSize(1);
-        assertThat(result.get(1).getUnencryptedObservations(0).getContributionId())
-                .isEqualTo(RANDOM_BYTES_4);
-        assertThat(result.get(1).getUnencryptedObservations(0).getObservation())
-                .isEqualTo(OBSERVATION_2);
-    }
-
-    @Test
-    public void generateObservations_eventVectorBufferMax_oneEventSent() throws Exception {
-        mGenerator =
-                createObservationGenerator(
-                        CUSTOMER,
-                        PROJECT,
-                        METRIC,
-                        REPORT.toBuilder().setEventVectorBufferMax(1).build());
-        List<UnencryptedObservationBatch> result =
-                mGenerator.generateObservations(
-                        DAY_INDEX,
-                        ImmutableListMultimap.of(
-                                SYSTEM_PROFILE_1, EVENT_1, SYSTEM_PROFILE_1, EVENT_2));
-
-        // Verify only the first event vector is aggregated into an observation.
-        assertThat(result).hasSize(1);
-        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
-        assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
-        assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
-                .isEqualTo(RANDOM_BYTES_2);
-        assertThat(result.get(0).getUnencryptedObservations(0).getObservation())
-                .isEqualTo(OBSERVATION_1);
-    }
-
-    @Test
-    public void generatePrivateObservations_noEvents_reportParticipationOnly() throws Exception {
-        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, PRIVATE_REPORT);
-        List<UnencryptedObservationBatch> result =
-                mGenerator.generateObservations(DAY_INDEX, ImmutableListMultimap.of());
 
         // Only a report participation observation is expected, as the report's lambda is too small
         // to trigger a fabricated observation.
         assertThat(result).hasSize(1);
         // Used the current system's SystemProfile, as no logged events have system profiles.
-        assertThat(result.get(0).getMetadata()).isEqualTo(PRIVATE_METADATA_1);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
         assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(1);
         assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
                 .isEqualTo(RANDOM_BYTES_2);
@@ -376,7 +199,7 @@
     }
 
     @Test
-    public void generatePrivateObservations_noEventsButFabricatedObservation_twoObservations()
+    public void generateObservations_noEventsButFabricatedObservation_twoObservations()
             throws Exception {
         mGenerator =
                 createObservationGenerator(
@@ -385,10 +208,7 @@
                         // lambda = poissonMean*((((maxEventVectorIndex+1)*numIndexPoints)-1)+1)
                         //        = poissonMean*((((0+1)*11)-1)+1) = poissonMean*11 >= 0.1
                         // poissonMean >= 0.0091
-                        CUSTOMER,
-                        PROJECT,
-                        METRIC,
-                        PRIVATE_REPORT.toBuilder().setPoissonMean(0.01).build());
+                        CUSTOMER, PROJECT, METRIC, REPORT.toBuilder().setPoissonMean(0.01).build());
         List<UnencryptedObservationBatch> result =
                 mGenerator.generateObservations(DAY_INDEX, ImmutableListMultimap.of());
 
@@ -396,7 +216,7 @@
         // batch.
         assertThat(result).hasSize(1);
         // Used the current system's SystemProfile, as no logged events have system profiles.
-        assertThat(result.get(0).getMetadata()).isEqualTo(PRIVATE_METADATA_1);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_1);
         assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(2);
         assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
                 .isEqualTo(RANDOM_BYTES_3);
@@ -421,9 +241,8 @@
     }
 
     @Test
-    public void generatePrivateObservations_oneEvent_generatedPlusReportParticipation()
-            throws Exception {
-        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, PRIVATE_REPORT);
+    public void generateObservations_oneEvent_generatedPlusReportParticipation() throws Exception {
+        mGenerator = createObservationGenerator(CUSTOMER, PROJECT, METRIC, REPORT);
         List<UnencryptedObservationBatch> result =
                 mGenerator.generateObservations(
                         DAY_INDEX,
@@ -434,7 +253,7 @@
         // small to trigger a fabricated observation.
         assertThat(result).hasSize(1);
         // All observations use the logged system profile.
-        assertThat(result.get(0).getMetadata()).isEqualTo(PRIVATE_METADATA_2);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_2);
         assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(2);
         assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
                 .isEqualTo(RANDOM_BYTES_3);
@@ -456,7 +275,7 @@
     }
 
     @Test
-    public void generatePrivateObservations_oneEventAndFabricatedObservation_threeObservations()
+    public void generateObservations_oneEventAndFabricatedObservation_threeObservations()
             throws Exception {
         mGenerator =
                 createObservationGenerator(
@@ -466,10 +285,7 @@
                         // lambda = poissonMean*((((maxEventVectorIndex+1)*numIndexPoints)-1)+1)
                         //        = poissonMean*((((0+1)*11)-1)+1) = poissonMean*11 >= 0.1
                         // poissonMean >= 0.0091
-                        CUSTOMER,
-                        PROJECT,
-                        METRIC,
-                        PRIVATE_REPORT.toBuilder().setPoissonMean(0.01).build());
+                        CUSTOMER, PROJECT, METRIC, REPORT.toBuilder().setPoissonMean(0.01).build());
         List<UnencryptedObservationBatch> result =
                 mGenerator.generateObservations(
                         DAY_INDEX,
@@ -480,7 +296,7 @@
         // batch.
         assertThat(result).hasSize(1);
         // All observations use the logged system profile.
-        assertThat(result.get(0).getMetadata()).isEqualTo(PRIVATE_METADATA_2);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_2);
         assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(3);
         assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
                 .isEqualTo(RANDOM_BYTES_4);
@@ -510,7 +326,7 @@
     }
 
     @Test
-    public void generatePrivateObservations_oneEventForMetricWithDimensions_threeObservations()
+    public void generateObservations_oneEventForMetricWithDimensions_threeObservations()
             throws Exception {
         mGenerator =
                 createObservationGenerator(
@@ -524,7 +340,7 @@
                         // lambda = poissonMean*((((maxEventVectorIndex+1)*numIndexPoints)-1)+1)
                         //        = poissonMean*((((5+1)*11)-1)+1) = poissonMean*66 >= 0.1
                         // poissonMean >= 0.00152
-                        PRIVATE_REPORT.toBuilder().setPoissonMean(0.002).build());
+                        REPORT.toBuilder().setPoissonMean(0.002).build());
         List<UnencryptedObservationBatch> result =
                 mGenerator.generateObservations(
                         DAY_INDEX, ImmutableListMultimap.of(SYSTEM_PROFILE_2, EVENT_1));
@@ -533,7 +349,7 @@
         // batch.
         assertThat(result).hasSize(1);
         // All observations use the logged system profile.
-        assertThat(result.get(0).getMetadata()).isEqualTo(PRIVATE_METADATA_2);
+        assertThat(result.get(0).getMetadata()).isEqualTo(METADATA_2);
         assertThat(result.get(0).getUnencryptedObservationsList()).hasSize(3);
         assertThat(result.get(0).getUnencryptedObservations(0).getContributionId())
                 .isEqualTo(RANDOM_BYTES_4);