Skip to content

[Analytics] Basic Consent migration control for legacy persisted collection enablement #8711

Open

Description

@YanlongLai

What feature would you like to see?

A documented, supported pre-initialization migration control for Basic Consent on an Android app upgrade when a previous release persisted setAnalyticsCollectionEnabled(true) and consent grants, but the replacement release requires a new explicit Analytics opt-in.

The Basic Consent guide documents both the Manifest default and the persisted setter taking precedence over that default. We are not claiming that precedence is itself a bug. The missing contract is how to safely migrate an existing installation before automatic-event admission, including when users skip intermediate app releases.

Ideally this would be a documented startup option that temporarily takes precedence over legacy persisted enablement until the application explicitly grants consent in the current process. Alternatively, please point us to an existing supported public-API sequence that provides that boundary. This is a request for SDK/API support, not a backend DebugView troubleshooting request. I understand Analytics implementation is not open source in this repository; please redirect if this SDK feature request belongs in another official tracker.

How would you use it?

Keep optional Analytics available after explicit opt-in in the same app version, while collecting no Analytics events before that new choice. FCM and consent-eligible advertising must remain independent; advertising eligibility is not Analytics consent. Permanent Analytics deactivation and switching silently to Advanced Consent would not meet this requirement.

Environment of the synthetic reproduction

  • Android Studio: not used; command-line Gradle build.
  • JDK 17, Gradle 8.14.3, Android Gradle Plugin 8.12.2.
  • compileSdk/targetSdk 36, minSdk 24.
  • Disposable ARM64 Google APIs/Play Store emulator, API 37.1.
  • Explicit Maven dependency com.google.firebase:firebase-analytics:23.2.0; dependency inspection resolved measurement dependencies to 23.2.0.
  • Also present: firebase-messaging:25.0.2, play-services-ads:25.0.0. No FCM token or ad request was made in the consent-first sequence described below.
  • That run selected the installed Google Play services remote measurement module, whose diagnostic version was 194. This is an internal module label, not a Maven SDK version. Exact bundled-module and older-Android equivalence have not been established.

Reproduction steps

  1. Use a disposable test application with a valid test google_app_id resource and normal Firebase initialization. Never use production user data. Keep the same application ID and signing certificate throughout.
  2. Install seed versionCode 1. Using only public APIs, grant all four consent purposes and call setAnalyticsCollectionEnabled(true). Launch and verify an explicit event and automatic events as positive controls. Force-stop afterward.
  3. Install a higher-version replacement APK using adb install -r without uninstalling or clearing data. Its Manifest has firebase_analytics_collection_enabled=false. A non-exported application ContentProvider with android:initOrder="1000" calls setConsent(all four DENIED) followed by setAnalyticsCollectionEnabled(false) before Application.onCreate. Inspect the merged Manifest: the ordinary FirebaseInitProvider in this fixture had initOrder 100.
  4. Cold-start the replacement without granting the new first-party opt-in. Compare frontend markers and package-attributed measurement-service diagnostics. Enable verbose FA/FA-SVC logs on the disposable emulator; do not rely on DebugView absence as proof of no event collection.
  5. As controls, compare with a legacy-enabled upgrade without the early-denial provider, and separately exercise same-process explicit grant, withdrawal, and denied restart. Restore the seed grants with public APIs before comparing another higher-version upgrade; do not edit SDK preference files.

These are excerpts of the exercised fixture, not an independently re-executed standalone sample project. No public reproduction repository or project configuration is attached.

Manifest excerpt from the consent-first fixture (inside <application>):

<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
<meta-data android:name="com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT" android:value="true" />
<provider
    android:name=".ConsentProvider"
    android:authorities="${applicationId}.consentprobe"
    android:exported="false"
    android:initOrder="1000" />

INTERNET permission was retained. Ads SDK configuration used Google's sample app ID. The fixture did not include the four Manifest consent-default keys; denial was applied through the public API on upgrade. Adding those defaults and migrating to other SDK/runtime versions are separate unverified cases, not results claimed here.

Core public-API helper, called with true by the seed and with false by the replacement provider:

static void applyConsent(Context context, boolean granted) {
    FirebaseAnalytics analytics = FirebaseAnalytics.getInstance(context);
    EnumMap<FirebaseAnalytics.ConsentType, FirebaseAnalytics.ConsentStatus> purposes =
        new EnumMap<>(FirebaseAnalytics.ConsentType.class);
    for (FirebaseAnalytics.ConsentType purpose : FirebaseAnalytics.ConsentType.values()) {
        purposes.put(purpose, granted
            ? FirebaseAnalytics.ConsentStatus.GRANTED
            : FirebaseAnalytics.ConsentStatus.DENIED);
    }
    Log.i("ConsentProbe", "before setConsent: " + granted);
    analytics.setConsent(purposes);
    analytics.setAnalyticsCollectionEnabled(granted);
    Log.i("ConsentProbe", "both public calls returned: " + granted);
}

Replacement provider excerpt (the other required ContentProvider overrides are no-ops):

@Override public boolean onCreate() {
    applyConsent(getContext(), false);
    return true;
}

All-purpose seed grants/denials are a synthetic diagnostic contrast, not a recommendation to couple advertising and Analytics choices in an application.

Observed ordering and limits

In the consent-first no-clear upgrade capture, taking the application's “both public calls returned: false” marker as T+0:

Relative time Observation
T+817 ms Frontend storage-denial processing log
T+939 ms Frontend collection-disabled processing log
T+956 ms Measurement service logs automatic app_update
T+1071 ms Measurement service logs package storage denial
T+1089 ms Measurement service logs DMA denial

Explicit application event markers afterward were rejected. The legacy-enabled positive control logged app_update. A later explicit grant admitted application markers; withdrawal rejected subsequent explicit markers, although already-accrued engagement was logged after withdrawal. A bounded denied-restart capture rejected explicit markers; that absence is not a universal no-collection proof.

These observations establish diagnostic event logging/admission order, not transmission of the pre-opt-in event, backend receipt, production-user impact, or an identifier leak. Network transport was not the acceptance measure. No raw user-device logs, project identifiers, credentials, or production configuration are attached.

Issue #5697 discusses events under storage-denied consent and DebugView visibility. This request differs: it concerns Basic collection disabled on a no-clear upgrade from persisted enabled/granted state, rather than expecting Advanced Consent to suppress all events.

Requested clarification / acceptance contract

  1. Is there a supported public startup/migration control that applies before measurement initialization reads a legacy persisted collection grant, without private preference writes, reflection, undocumented Bundle keys, or a custom ClassLoader?
  2. If an early public getter/setter is the supported approach, what guarantees ordering across the frontend and the Play services measurement process, and how should an app verify that boundary?
  3. If this is not supported today, could you add a migration option or document the limitation and the supported alternative? The solution should allow same-release explicit grant/withdraw/restart and users skipping intermediate releases, without disabling unrelated Firebase products.

Activity

  1. ProfSeaquill commented on Oct 28, 2026

    @ProfSeaquill

    Additional synthetic native reproduction for the legacy persisted-ON migration case, including a provider-before-Activity experiment. This adds an Android 35 / local-measurement-module environment and paired measurement-service database evidence.

    Scope: this concerns a legacy SDK grant with unknown consent provenance. It does not imply that valid affirmative consent must be revoked every launch, nor establish production-user impact or universal impossibility.

    Environment and preconditions

    • Firebase Analytics 23.2.0, Firebase Common 22.1.0; Crashlytics NDK 20.1.0 and Ads 25.4.0 / transitive UMP 4.0.0 also present. No UMP request or ad request was made in the candidate.
    • Android 35 Google ATD ARM64 emulator; GMS 24.23.35 (190408-646585959); selected local measurement module 185. The module label is not the Maven SDK version.
    • Fake Firebase/sample Ads configuration, no production project or real player data. IPv4/IPv6 egress was denied before installation and throughout the run. This is local collection/admission evidence, not cloud receipt or transmission evidence.
    • Public API seed: Analytics storage GRANTED, collection ON, then a late grant and a non-null public app-instance ID as positive controls. Read-only copied preferences confirmed G101, measurement_enabled=true and measurement_enabled_from_api=true. These prove SDK state, not affirmative user consent.
    • Candidate preserved app data, versionCode and versionName; this is a cold-start case, not an app-update case. No private SDK preference/database writes.

    Candidate

    Remove FirebaseInitProvider. Our non-exported provider has initOrder=1000. Its onCreate initializes Firebase, immediately disables collection through the public API, requests getSessionId() with an explicit background completion executor, then waits only for the remaining portion of a two-second deadline measured before initialization. No Firebase calls occur in the Application/Activity callbacks.

    Relevant manifest metadata:

    <meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
    <meta-data android:name="firebase_crashlytics_collection_enabled" android:value="false" />
    <meta-data android:name="google_analytics_default_allow_analytics_storage" android:value="false" />
    <meta-data android:name="google_analytics_default_allow_ad_storage" android:value="false" />
    <provider android:name="com.google.firebase.provider.FirebaseInitProvider"
        android:authorities="${applicationId}.firebaseinitprovider"
        tools:node="remove" />

    Core sequence (abridged from the exercised fixture; not a separately rerun standalone sample):

    long deadlineNs = SystemClock.elapsedRealtimeNanos() + TimeUnit.SECONDS.toNanos(2);
    ExecutorService completionExecutor = Executors.newSingleThreadExecutor();
    CountDownLatch completed = new CountDownLatch(1);
    
    FirebaseApp.initializeApp(context);
    FirebaseAnalytics analytics = FirebaseAnalytics.getInstance(context);
    analytics.setAnalyticsCollectionEnabled(false);
    analytics.getSessionId().addOnCompleteListener(completionExecutor, task -> {
        // Record elapsed timestamp, success/null result and callback thread.
        completed.countDown();
        completionExecutor.shutdown();
    });
    completed.await(Math.max(0, deadlineNs - SystemClock.elapsedRealtimeNanos()),
                    TimeUnit.NANOSECONDS);
    return true; // Provider returns; normal Activity lifecycle can proceed.

    The actual fixture also records callback status and deadlines separately. We do not assume getter completion acknowledges collection OFF or provides a documented synchronization contract. This was an empirical ordering experiment, not a proposed production workaround. Neither consent denial nor reset was invoked in this candidate.

    Result

    Relative to public collection-OFF return:

    Time Observation
    +660 ms SDK detects process already foreground
    +661 ms New raw _s event timestamp
    +663 ms Client logs session started
    +842 ms Worker applies collection OFF
    +869 ms Genuine getter completion observed / provider wait returns
    +875 ms Provider exits
    +999 ms First Activity onCreate
    +1087 ms First Activity resumes
    +1507 ms GMS logs session_start ingestion

    Getter completion was successful, non-null, on pool-2-thread-1, about 596 ms before the explicit deadline. Provider duration was about 1409 ms. The failure was therefore not a timeout or a main-thread completion-listener deadlock: the automatic session was generated before the fence completed and before any Activity lifecycle.

    Paired read-only copies of the measurement-service database show raw rows 5 → 6. The only new row was _s, payload SHA256:

    ccaaee6b70172d0f9735688d6824480ea3de59c22a535a393ca5619bb09e383b

    No additional app-update/first-open row or queued upload bundle appeared in this case. An ADB-listener interruption delayed final capture; recovery copied the same uninterrupted candidate process, without relaunching the Activity/app or running another case. The ordering logs were already captured before that interruption.

    We also separately tried removing FirebaseInitProvider and deferring first Firebase construction until after the first Activity resume, immediately followed by public collection OFF. That arrangement also admitted a new automatic session before worker OFF.

    Clarification requested

    Is there a supported startup/migration control that supersedes legacy unknown-provenance persisted enablement before automatic event admission, while still allowing collection after a valid decision in the same release?

    If the correct approach uses a different public API or initialization arrangement, what is its ordering contract across the client and Play services measurement process? We want to avoid private preference writes, undocumented Bundle keys, getter-as-ack assumptions, provider blocking, and treating a multi-release migration as safe when users can skip intermediate releases.

    No production configuration, credentials, real-user device logs or database payloads are attached.

  2. YanlongLai commented on Oct 30, 2026

    @YanlongLai
    Author

    @ProfSeaquill Thanks for the additional reproduction and for clearly separating local event admission from transmission or backend receipt.

    The provider-before-Activity result is particularly useful: in this capture, the new _s event precedes worker-side collection OFF and the first Activity callback, despite the public setter having returned. Getter completion therefore does not resolve the startup boundary being requested here. This remains evidence for the tested configuration, rather than proof that every initialization arrangement behaves identically.

    For the Firebase Analytics team, could you clarify whether there is a supported public initialization or migration mechanism that prevents automatic event admission when an existing installation has persisted collection ON but the app cannot establish the provenance of that consent? The requirement is to keep Analytics disabled until a valid choice, while allowing opt-in in the same release and keeping unrelated Firebase products usable, including for users who skip intermediate releases.

    If such a mechanism exists, please point us to its documented ordering guarantees, including the Play services measurement process. If it does not, documenting that limitation and the supported migration alternative would help. We are not proposing provider blocking or getter completion as a production synchronization contract, and this request does not require revoking valid affirmative consent on every launch.

  3. lehcar09 commented on Oct 30, 2026

    @lehcar09
    Contributor

    Hi @YanlongLai, thank you for reaching out and providing a detailed report.

    Currently, there's no documented startup or migration option that override a previously saved setAnalyticsCollectionEnabled(true) before Analytics starts. There's also no public-API sequence we can point to that guarantees no automatic events are recorded before a new opt-in. The setter's value persists across app launches and overrides the Manifest default until it's called again (Ref). The only documented control that outranks it is by adding the firebase_analytics_collection_deactivated in the manifest file, which is permanent for that app version so it doesn't let users opt in with the same release.

    The order you observed seems to coincide with app_update event; Analytics check for it at startup by comparing the current app version with the last version on the device. Calling setContent or setAnalyticsCollectionEnabled(false) early isn't documented to take effect before the check.

    I agree that this is a gap for Basic Consent migrations. I'll raise this to our engineers as a feature request covering a migration option and/or documentation of the limitation.

    One note on the fixture: withdrawing Analytics or Ad storage consent deletes all user properties, including ad personalization consent. If advertising choices need to stay independent, restore that value with setConsent

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions