IJPL-236763 Ensure TRACE data collection requires AI Assistant plugin

- Updated handling in GDPR consent UI to enforce TRACE data collection dependency on the availability of the AI Assistant plugin
- Adjusted tests to verify forced disabling of TRACE consent when the AI Assistant plugin is unavailable or forcibly disabled
- Added Mockito dependency for mocking plugin states in unit tests

IJ-CR-194089

(cherry picked from commit 7284d71a054448d67c63e7d32ea59a65a612e8bd)

GitOrigin-RevId: bae064b033e5ae8a0559dd0b6a483315fec69573
This commit is contained in:
Ilia Smirnov
2026-03-02 21:40:02 +00:00
committed by intellij-monorepo-bot
parent 859db02320
commit 2ec242a01d
7 changed files with 70 additions and 11 deletions
@@ -2738,6 +2738,7 @@ gdpr.ai.data.collection.consent.additional.notice.2=\
Even if this setting is disabled, the AI Assistant plugin will send the data essential for this feature to large language model providers \
and models hosted on JetBrains servers. If you work on a project where you don't want to share your data, you can disable the plugin.
gdpr.consent.externally.disabled.warning=This option is disabled externally
gdpr.consent.trace.requires.ai.assistant=This option requires the AI Assistant plugin
gdpr.data.sharing.title.comment.text=Help shape the future of {0} products. By sharing your data and usage statistics, you allow us to better understand how you use our tools and how we can improve them. Learn more <a href="https://www.jetbrains.com/legal/docs/terms/product_data_collection/">here</a>.
gdpr.data.collection.consent.setting.enabled.warning.text=Data collection has been enabled by your organization administrator.
gdpr.data.collection.consent.setting.disabled.warning.text=Data collection has been disabled by your organization administrator.
@@ -63,8 +63,11 @@ final class ConsentOptionsProviderImpl implements ConsentOptionsProvider {
return false;
}
AiDataCollectionExternalSettings settings = AiDataCollectionExternalSettings.findSettingsImplementedByAiAssistant();
if (settings == null) {
return false; // AIA plugin is required for TRACE data collection
}
boolean isAllowed = isTraceDataCollectionAllowedByMetadata(metadata);
boolean isDisabled = settings != null && settings.isForciblyDisabled();
boolean isDisabled = settings.isForciblyDisabled();
return isAllowed && !isDisabled;
}
@@ -2,8 +2,11 @@
package com.intellij.ide.gdpr.ui.consents
import com.intellij.internal.statistic.utils.getPluginInfo
import com.intellij.openapi.Disposable
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.util.Disposer
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
@ApiStatus.Internal
interface AiDataCollectionExternalSettings {
@@ -13,8 +16,18 @@ interface AiDataCollectionExternalSettings {
private const val AI_ASSISTANT_PLUGIN_ID = "com.intellij.ml.llm"
private var testOverride: AiDataCollectionExternalSettings? = null
@TestOnly
@JvmStatic
fun overrideForTest(settings: AiDataCollectionExternalSettings, parentDisposable: Disposable) {
testOverride = settings
Disposer.register(parentDisposable) { testOverride = null }
}
@JvmStatic
fun findSettingsImplementedByAiAssistant(): AiDataCollectionExternalSettings? {
testOverride?.let { return it }
return EP_NAME.findFirstSafe {
val pluginInfo = getPluginInfo(it.javaClass)
pluginInfo.isDevelopedByJetBrains() && pluginInfo.id == AI_ASSISTANT_PLUGIN_ID
@@ -22,15 +22,17 @@ internal class TraceDataCollectionConsentUI(
if (LOG.isDebugEnabled) {
LOG.debug("AiDataCollectionExternalSettings: $externalSettings")
}
if (externalSettings != null) {
val isForciblyDisabled = externalSettings.isForciblyDisabled()
if (LOG.isDebugEnabled) {
LOG.debug("AiDataCollectionExternalSettings: isForciblyDisabled: ${isForciblyDisabled}")
}
if (isForciblyDisabled) {
return ConsentForcedState.ExternallyDisabled(externalSettings.getForciblyDisabledDescription()
?: IdeBundle.message("gdpr.consent.externally.disabled.warning"))
}
if (externalSettings == null) {
// AIA plugin is required for TRACE data collection
return ConsentForcedState.ExternallyDisabled(IdeBundle.message("gdpr.consent.trace.requires.ai.assistant"))
}
val isForciblyDisabled = externalSettings.isForciblyDisabled()
if (LOG.isDebugEnabled) {
LOG.debug("AiDataCollectionExternalSettings: isForciblyDisabled: $isForciblyDisabled")
}
if (isForciblyDisabled) {
return ConsentForcedState.ExternallyDisabled(externalSettings.getForciblyDisabledDescription()
?: IdeBundle.message("gdpr.consent.externally.disabled.warning"))
}
val dataCollectionAgreement = DataCollectionAgreement.getInstance()
return when (dataCollectionAgreement) {
+1
View File
@@ -189,6 +189,7 @@ jvm_library(
"//platform/platform-tests/eel-helper",
"//platform/platform-impl/ui",
"//platform/eel-provider",
"//libraries/mockito",
"@lib//:io-mockk",
"@lib//:io-mockk-jvm",
"//platform/bookmarks",
@@ -138,6 +138,7 @@
<orderEntry type="module" module-name="intellij.platform.eelHelper" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.ide.ui" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.eel.provider" scope="TEST" />
<orderEntry type="module" module-name="intellij.libraries.mockito" scope="TEST" />
<orderEntry type="library" scope="TEST" name="io.mockk" level="project" />
<orderEntry type="library" scope="TEST" name="io.mockk.jvm" level="project" />
<orderEntry type="module" module-name="intellij.platform.bookmarks" scope="TEST" />
@@ -1,10 +1,13 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.gdpr;
import com.intellij.ide.IdeBundle;
import com.intellij.ide.gdpr.ui.consents.AiDataCollectionExternalSettings;
import com.intellij.ide.gdpr.ui.consents.ConsentForcedState;
import com.intellij.ide.gdpr.ui.consents.ConsentUi;
import com.intellij.testFramework.fixtures.BasePlatformTestCase;
import com.intellij.ui.LicensingFacade;
import org.mockito.Mockito;
public class DataCollectionConsentUiTest extends BasePlatformTestCase {
private static final String CONSENT_ID_USAGE_STATS = "rsch.send.usage.stat";
@@ -66,6 +69,10 @@ public class DataCollectionConsentUiTest extends BasePlatformTestCase {
false, "en");
ConsentUi ui = ConsentSettingsUi.getConsentUi(trace);
AiDataCollectionExternalSettings mockSettings = Mockito.mock(AiDataCollectionExternalSettings.class);
Mockito.doReturn(false).when(mockSettings).isForciblyDisabled();
AiDataCollectionExternalSettings.overrideForTest(mockSettings, getTestRootDisposable());
setupLicensingFacade('X');
assertNull(ui.getForcedState());
@@ -81,4 +88,35 @@ public class DataCollectionConsentUiTest extends BasePlatformTestCase {
assertInstanceOf(state, ConsentForcedState.AlwaysEnabled.class);
assertEquals("Data collection has been enabled by your organization administrator.", state.getDescription());
}
}
public void testTraceConsentForcedDisabledWhenAiaPluginAbsent() {
Consent trace = new Consent(CONSENT_ID_TRACE_DATA_COM_COLLECTION, Version.fromString("1.0"), "Send detailed code-related data", "text", false,
false, "en");
ConsentUi ui = ConsentSettingsUi.getConsentUi(trace);
for (char agreementChar : new char[]{'X', 'N', 'Y'}) {
setupLicensingFacade(agreementChar);
var state = ui.getForcedState();
assertNotNull("TRACE consent must be force-disabled when AIA plugin is absent (agreement='" + agreementChar + "')", state);
assertInstanceOf(state, ConsentForcedState.ExternallyDisabled.class);
assertEquals(IdeBundle.message("gdpr.consent.trace.requires.ai.assistant"), state.getDescription());
}
}
public void testTraceConsentForcedDisabledWhenAiaForciblyDisabled() {
Consent trace = new Consent(CONSENT_ID_TRACE_DATA_COM_COLLECTION, Version.fromString("1.0"), "Send detailed code-related data", "text", false,
false, "en");
ConsentUi ui = ConsentSettingsUi.getConsentUi(trace);
AiDataCollectionExternalSettings mockSettings = Mockito.mock(AiDataCollectionExternalSettings.class);
Mockito.doReturn(true).when(mockSettings).isForciblyDisabled();
Mockito.doReturn("Disabled by organization").when(mockSettings).getForciblyDisabledDescription();
AiDataCollectionExternalSettings.overrideForTest(mockSettings, getTestRootDisposable());
setupLicensingFacade('X');
var state = ui.getForcedState();
assertNotNull(state);
assertInstanceOf(state, ConsentForcedState.ExternallyDisabled.class);
assertEquals("Disabled by organization", state.getDescription());
}
}