IJPL-208070 Split the TRACE data collection consent into two versions: a non-commercial version and an "all other" one

- adjust Settings, Event logging logic and notification display logic based on the TRACE consents state
- simplify check for the personal license
- enhance and adjust tests

(cherry picked from commit d0f0bc2e8206fe89a588417723d25b6656598b7b)

IJ-CR-177425

GitOrigin-RevId: 66fbc9c8ea5310a1e6bbb488aa38e28dec2504b0
This commit is contained in:
Ilia Smirnov
2025-09-18 16:16:17 +02:00
committed by intellij-monorepo-bot
parent 1bdb967491
commit 913379768a
7 changed files with 116 additions and 27 deletions

View File

@@ -34,7 +34,8 @@ public final class ConsentOptions implements ModificationTracker {
private static final String STATISTICS_OPTION_ID = "rsch.send.usage.stat";
private static final String EAP_FEEDBACK_OPTION_ID = "eap";
private static final String AI_DATA_COLLECTION_OPTION_ID = "ai.data.collection.and.use.policy";
private static final String TRACE_DATA_COLLECTION_OPTION_ID = "ai.trace.data.collection.and.use.policy";
private static final String TRACE_DATA_COLLECTION_NON_COM_OPTION_ID = "ai.trace.data.collection.and.use.noncom.policy";
private static final String TRACE_DATA_COLLECTION_COM_OPTION_ID = "ai.trace.data.collection.and.use.com.policy";
private static final Set<String> PER_PRODUCT_CONSENTS = Set.of(EAP_FEEDBACK_OPTION_ID);
private final BooleanSupplier myIsEap;
private String myProductCode;
@@ -201,8 +202,12 @@ public final class ConsentOptions implements ModificationTracker {
return consent -> AI_DATA_COLLECTION_OPTION_ID.equals(consent.getId());
}
public static @NotNull Predicate<Consent> condTraceDataCollectionConsent() {
return consent -> TRACE_DATA_COLLECTION_OPTION_ID.equals(consent.getId());
public static @NotNull Predicate<Consent> condTraceDataCollectionNonComConsent() {
return consent -> TRACE_DATA_COLLECTION_NON_COM_OPTION_ID.equals(consent.getId());
}
public static @NotNull Predicate<Consent> condTraceDataCollectionComConsent() {
return consent -> TRACE_DATA_COLLECTION_COM_OPTION_ID.equals(consent.getId());
}
/**
@@ -238,12 +243,20 @@ public final class ConsentOptions implements ModificationTracker {
setPermission(AI_DATA_COLLECTION_OPTION_ID, permitted);
}
public @NotNull Permission getTraceDataCollectionPermission() {
return getPermission(TRACE_DATA_COLLECTION_OPTION_ID);
public @NotNull Permission getTraceDataCollectionNonComPermission() {
return getPermission(TRACE_DATA_COLLECTION_NON_COM_OPTION_ID);
}
public void setTraceDataCollectionPermission(boolean permitted) {
setPermission(TRACE_DATA_COLLECTION_OPTION_ID, permitted);
public void setTraceDataCollectionNonComPermission(boolean permitted) {
setPermission(TRACE_DATA_COLLECTION_NON_COM_OPTION_ID, permitted);
}
public @NotNull Permission getTraceDataCollectionComPermission() {
return getPermission(TRACE_DATA_COLLECTION_COM_OPTION_ID);
}
public void setTraceDataCollectionComPermission(boolean permitted) {
setPermission(TRACE_DATA_COLLECTION_COM_OPTION_ID, permitted);
}
private @NotNull Permission getPermission(final String consentId) {

View File

@@ -4,11 +4,13 @@ package com.intellij.ide.gdpr;
import com.intellij.ide.ConsentOptionsProvider;
import com.intellij.ide.gdpr.ui.consents.AiDataCollectionExternalSettings;
import com.intellij.ui.LicensingFacade;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
final class ConsentOptionsProviderImpl implements ConsentOptionsProvider {
private static final Set<String> productsSupportingForcedConsent = Set.of("QA", "RR", "WS", "RD", "CL", "RM", "DB");
private static final int METADATA_LICENSE_TYPE_INDEX = 10;
private volatile long myLastModificationCount = -1;
private volatile boolean mySendingAllowed = false;
@@ -26,7 +28,7 @@ final class ConsentOptionsProviderImpl implements ConsentOptionsProvider {
return false;
}
String meta = facade.metadata;
return meta != null && meta.length() > 10 && meta.charAt(10) == 'F';
return meta != null && meta.length() > METADATA_LICENSE_TYPE_INDEX && meta.charAt(METADATA_LICENSE_TYPE_INDEX) == 'F';
}
@Override
@@ -50,11 +52,29 @@ final class ConsentOptionsProviderImpl implements ConsentOptionsProvider {
@Override
public boolean isTraceDataCollectionAllowed() {
DataCollectionAgreement dataCollectionAgreement = DataCollectionAgreement.getInstance();
LicensingFacade facade = LicensingFacade.getInstance();
if (facade == null) {
return false;
}
String metadata = facade.metadata;
if (metadata == null) {
return false;
}
AiDataCollectionExternalSettings settings = AiDataCollectionExternalSettings.findSettingsImplementedByAiAssistant();
boolean isAllowed = dataCollectionAgreement == DataCollectionAgreement.YES ||
ConsentOptions.getInstance().getTraceDataCollectionPermission() == ConsentOptions.Permission.YES;
boolean isAllowed = isTraceDataCollectionAllowedByMetadata(metadata);
boolean isDisabled = settings != null && settings.isForciblyDisabled();
return isAllowed && !isDisabled;
}
private static boolean isTraceDataCollectionAllowedByMetadata(@NotNull String metadata) {
if (metadata.length() <= METADATA_LICENSE_TYPE_INDEX) {
return false;
}
DataCollectionAgreement dataCollectionAgreement = DataCollectionAgreement.getInstance();
ConsentOptions.Permission traceDataCollectionPermission = metadata.charAt(METADATA_LICENSE_TYPE_INDEX) == 'F'
? ConsentOptions.getInstance().getTraceDataCollectionNonComPermission()
: ConsentOptions.getInstance().getTraceDataCollectionComPermission();
return dataCollectionAgreement == DataCollectionAgreement.YES ||
traceDataCollectionPermission == ConsentOptions.Permission.YES;
}
}

View File

@@ -107,7 +107,8 @@ public class ConsentSettingsUi extends JPanel implements ConfigurableUi<List<Con
if (ConsentOptions.condUsageStatsConsent().test(consent)) {
return new UsageStatisticsConsentUi(consent);
}
if (ConsentOptions.condTraceDataCollectionConsent().test(consent)) {
if (ConsentOptions.condTraceDataCollectionComConsent().test(consent) ||
ConsentOptions.condTraceDataCollectionNonComConsent().test(consent)) {
return new TraceDataCollectionConsentUI(consent);
}
return new DefaultConsentUi(consent);

View File

@@ -16,7 +16,8 @@ internal class ConsentGroup(
val CONSENT_GROUP_MAPPING: Map<String, (Consent) -> Boolean> = mapOf(
DATA_COLLECTION_GROUP_ID to { consent ->
ConsentOptions.condUsageStatsConsent().test(consent) ||
ConsentOptions.condTraceDataCollectionConsent().test(consent)
ConsentOptions.condTraceDataCollectionComConsent().test(consent) ||
ConsentOptions.condTraceDataCollectionNonComConsent().test(consent)
}
)
}

View File

@@ -185,6 +185,13 @@ fun isWindowIconAlreadyExternallySet(): Boolean {
}
}
private fun removeTraceConsents(consents: MutableList<Consent>) {
consents.removeIf { consent ->
ConsentOptions.condTraceDataCollectionNonComConsent().test(consent) ||
ConsentOptions.condTraceDataCollectionComConsent().test(consent)
}
}
object AppUIUtil {
@JvmStatic
fun loadApplicationIcon(ctx: ScaleContext, size: Int): Icon? {
@@ -354,7 +361,14 @@ object AppUIUtil {
result.removeIf(isLegacyAiDataCollectionConsent) // IJPL-195651; AI data collection (LLMC) consent should not be present on UI while it's staying a default consent as a part of migration from LLMC to TRACE consent
}
if (traceConsentManager?.canDisplayTraceConsent() != true) {
result.removeIf(ConsentOptions.condTraceDataCollectionConsent())
removeTraceConsents(result)
} else {
val licenseTypeFlag = LicensingFacade.getInstance()?.metadata?.getOrNull(10)
when (licenseTypeFlag) {
'F' -> result.removeIf(ConsentOptions.condTraceDataCollectionComConsent())
null -> removeTraceConsents(result)
else -> result.removeIf(ConsentOptions.condTraceDataCollectionNonComConsent())
}
}
return result
}

View File

@@ -4,7 +4,9 @@ package com.intellij.ide.gdpr;
import com.intellij.ide.gdpr.ui.consents.ConsentGroup;
import com.intellij.ide.gdpr.ui.consents.DataCollectionConsentGroupUI;
import com.intellij.openapi.util.text.StringUtil;
import junit.framework.TestCase;
import com.intellij.testFramework.junit5.TestApplication;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import kotlin.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -15,26 +17,39 @@ import java.util.List;
/**
* @author Eugene Zhuravlev
*/
public class ConsentsTest extends TestCase{
@TestApplication
public class ConsentsTest {
private static final String JSON_CONSENTS_DATA = "[{\"consentId\":\"rsch.test.consent.option.for.intellij\",\"version\":\"1.0\",\"text\":\"This is a text of test consent option.\",\"printableName\":\"Test consent option\",\"accepted\":true,\"deleted\":false,\"acceptanceTime\":0},{\"consentId\":\"rsch.send.usage.stat\",\"version\":\"1.0\",\"text\":\"I consent to submit anonymous usage statistics to help JetBrains make better releases and refine the most important areas of the products. I agree that the following information will be sent\\n * Information about which product features is used\\n * General statistics (number of files, file types) of the solutions I am working on\\n * General information about my hardware configuration (for example, amount of RAM, CPU speed and number of cores)\\n * General information about my software configuration (for example, OS version)\",\"printableName\":\"Send anonymous usage statistics to JetBrains\",\"accepted\":false,\"deleted\":false,\"acceptanceTime\":0}]";
private static final String JSON_MINOR_UPGRADE_CONSENTS_DATA = "[{\"consentId\":\"rsch.test.consent.option.for.intellij\",\"version\":\"1.5\",\"text\":\"This is an upgraded text of test consent option.\",\"printableName\":\"Test consent option\",\"accepted\":true,\"deleted\":false,\"acceptanceTime\":0}]";
private static final String JSON_MAJOR_UPGRADE_CONSENTS_DATA = "[{\"consentId\":\"rsch.send.usage.stat\",\"version\":\"2.0\",\"text\":\"This is an major-upgraded text of usage stats option.\",\"printableName\":\"Test consent option\",\"accepted\":true,\"deleted\":false,\"acceptanceTime\":0}]";
private static final String JSON_DATA_COLLECTION_CONSENTS_DATA = "[" +
"{\"consentId\":\"rsch.send.usage.stat\",\"version\":\"1.1\",\"text\":\"This information includes, but is not limited to, anonymous data about your feature and plugin usage, hardware and software configuration, file type statistics, and the number of files per project. No personal data or sensitive information, such as source code or file names, is shared with us.\",\"printableName\":\"Send anonymous usage statistics\",\"accepted\":false}," +
"{\"consentId\":\"ai.trace.data.collection.and.use.policy\",\"version\":\"1.0\",\"text\":\"This includes an expanded range of IDE data with associated code snippets, such as AI feature usage, run configurations, and terminal commands. This data will be used for product improvement and model training purposes.\",\"printableName\":\"Send detailed code-related data\",\"accepted\":false}]";
private static final String JSON_DATA_COLLECTION_GROUP_COM_CONSENTS_DATA = "[" +
"{\"consentId\":\"rsch.send.usage.stat\",\"version\":\"1.1\",\"text\":\"This information includes, but is not limited to, anonymous data about your feature and plugin usage, hardware and software configuration, file type statistics, and the number of files per project. No personal data or sensitive information, such as source code or file names, is shared with us.\",\"printableName\":\"Send anonymous usage statistics\",\"accepted\":false}," +
"{\"consentId\":\"ai.trace.data.collection.and.use.com.policy\",\"version\":\"1.0\",\"text\":\"This includes an expanded range of IDE data with associated code snippets, such as AI feature usage, run configurations, and terminal commands. This data will be used for product improvement and model training purposes.\",\"printableName\":\"Send detailed code-related data\",\"accepted\":false}]";
private static final String JSON_DATA_COLLECTION_GROUP_NON_COM_CONSENTS_DATA = "[" +
"{\"consentId\":\"rsch.send.usage.stat\",\"version\":\"1.1\",\"text\":\"This information includes, but is not limited to, anonymous data about your feature and plugin usage, hardware and software configuration, file type statistics, and the number of files per project. No personal data or sensitive information, such as source code or file names, is shared with us.\",\"printableName\":\"Send anonymous usage statistics\",\"accepted\":false}," +
"{\"consentId\":\"ai.trace.data.collection.and.use.noncom.policy\",\"version\":\"1.0\",\"text\":\"This includes an expanded range of IDE data with associated code snippets, such as AI feature usage, run configurations, and terminal commands. This data will be used for product improvement and model training purposes.\",\"printableName\":\"Send detailed code-related data\",\"accepted\":false}]";
private static final String CONSENT_ID_1 = "rsch.test.consent.option.for.intellij";
private static final String CONSENT_ID_USAGE_STATS = "rsch.send.usage.stat";
private static final String CONSENT_ID_TRACE_DATA_COLLECTION = "ai.trace.data.collection.and.use.policy";
private static final String CONSENT_ID_TRACE_DATA_COLLECTION_COM = "ai.trace.data.collection.and.use.com.policy";
private static final String CONSENT_ID_TRACE_DATA_COLLECTION_NON_COM = "ai.trace.data.collection.and.use.noncom.policy";
private static final String GROUP_CONSENT_ID_DATA_COLLECTION = "data.collection";
// Compatibility helpers to keep a legacy assertion call style while using JUnit 5
private static void assertTrue(String message, boolean condition) { org.junit.jupiter.api.Assertions.assertTrue(condition, message); }
private static void assertFalse(String message, boolean condition) { org.junit.jupiter.api.Assertions.assertFalse(condition, message); }
private static void assertTrue(boolean condition) { org.junit.jupiter.api.Assertions.assertTrue(condition); }
private static void assertFalse(boolean condition) { org.junit.jupiter.api.Assertions.assertFalse(condition); }
private static String createUpgradeJson(String id, boolean isAccepted) {
final long tstamp = System.currentTimeMillis();
return "[{\"consentId\":\"" + id + "\",\"version\":\"1.5\",\"text\":\"This is an upgraded text of test consent option.\",\"printableName\":\"Test consent option\",\"accepted\":" +
isAccepted + ",\"deleted\":false,\"acceptanceTime\":" + tstamp + "}]";
}
@Test
public void testUpdateDefaultsAndConfirmedFromServer() throws InterruptedException {
final Pair<ConsentOptions, MemoryIOBackend> data = createConsentOptions("", JSON_CONSENTS_DATA);
final ConsentOptions options = data.getFirst();
@@ -77,6 +92,7 @@ public class ConsentsTest extends TestCase{
}
}
@Test
public void testConsentMinorVersionUpgrade() {
final Pair<ConsentOptions, MemoryIOBackend> data = createConsentOptions("", JSON_CONSENTS_DATA);
final ConsentOptions options = data.getFirst();
@@ -111,6 +127,7 @@ public class ConsentsTest extends TestCase{
assertEquals(Version.fromString("1.5"), consentAfterUpgrade.getVersion());
}
@Test
public void testConsentMajorVersionUpgrade() {
final Pair<ConsentOptions, MemoryIOBackend> data = createConsentOptions("", JSON_CONSENTS_DATA);
final ConsentOptions options = data.getFirst();
@@ -185,6 +202,7 @@ public class ConsentsTest extends TestCase{
}
}
@Test
public void testUsageStatsPermission() {
final Pair<ConsentOptions, MemoryIOBackend> data = createConsentOptions(JSON_CONSENTS_DATA, JSON_CONSENTS_DATA);
final ConsentOptions options = data.getFirst();
@@ -209,6 +227,7 @@ public class ConsentsTest extends TestCase{
assertEquals(ConsentOptions.Permission.NO, options.isSendingUsageStatsAllowed());
}
@Test
public void testLoadReadAndConfirm() {
final Pair<ConsentOptions, MemoryIOBackend> data = createConsentOptions(JSON_CONSENTS_DATA, JSON_CONSENTS_DATA);
final ConsentOptions options = data.getFirst();
@@ -241,8 +260,9 @@ public class ConsentsTest extends TestCase{
}
}
public void testDataCollectionConsentGroup() {
final Pair<ConsentOptions, MemoryIOBackend> data = createConsentOptions("", JSON_DATA_COLLECTION_CONSENTS_DATA);
@Test
public void testDataCollectionComConsentGroup() {
final Pair<ConsentOptions, MemoryIOBackend> data = createConsentOptions("", JSON_DATA_COLLECTION_GROUP_COM_CONSENTS_DATA);
final ConsentOptions options = data.getFirst();
final Pair<List<Consent>, Boolean> beforeConfirm = options.getConsents();
@@ -250,11 +270,31 @@ public class ConsentsTest extends TestCase{
assertEquals(2, beforeConfirm.getFirst().size());
final Consent fusConsent = lookupConsent(CONSENT_ID_USAGE_STATS, beforeConfirm.getFirst());
final Consent traceConsent = lookupConsent(CONSENT_ID_TRACE_DATA_COLLECTION, beforeConfirm.getFirst());
final Consent traceConsent = lookupConsent(CONSENT_ID_TRACE_DATA_COLLECTION_COM, beforeConfirm.getFirst());
assertNotNull(fusConsent);
assertNotNull(traceConsent);
assertTrue(ConsentOptions.condUsageStatsConsent().test(fusConsent));
assertTrue(ConsentOptions.condTraceDataCollectionConsent().test(traceConsent));
assertTrue(ConsentOptions.condTraceDataCollectionComConsent().test(traceConsent));
ConsentGroup group = new ConsentGroup(GROUP_CONSENT_ID_DATA_COLLECTION, beforeConfirm.getFirst());
assertSame(DataCollectionConsentGroupUI.class, ConsentSettingsUi.getConsentGroupUi(group).getClass());
}
@Test
public void testDataCollectionNonComConsentGroup() {
final Pair<ConsentOptions, MemoryIOBackend> data = createConsentOptions("", JSON_DATA_COLLECTION_GROUP_NON_COM_CONSENTS_DATA);
final ConsentOptions options = data.getFirst();
final Pair<List<Consent>, Boolean> beforeConfirm = options.getConsents();
assertTrue("Consents should require confirmation", beforeConfirm.getSecond());
assertEquals(2, beforeConfirm.getFirst().size());
final Consent fusConsent = lookupConsent(CONSENT_ID_USAGE_STATS, beforeConfirm.getFirst());
final Consent traceConsent = lookupConsent(CONSENT_ID_TRACE_DATA_COLLECTION_NON_COM, beforeConfirm.getFirst());
assertNotNull(fusConsent);
assertNotNull(traceConsent);
assertTrue(ConsentOptions.condUsageStatsConsent().test(fusConsent));
assertTrue(ConsentOptions.condTraceDataCollectionNonComConsent().test(traceConsent));
ConsentGroup group = new ConsentGroup(GROUP_CONSENT_ID_DATA_COLLECTION, beforeConfirm.getFirst());
assertSame(DataCollectionConsentGroupUI.class, ConsentSettingsUi.getConsentGroupUi(group).getClass());

View File

@@ -13,7 +13,7 @@ import java.util.List;
public class DataCollectionConsentUiTest extends BasePlatformTestCase {
private static final String CONSENT_ID_USAGE_STATS = "rsch.send.usage.stat";
private static final String CONSENT_ID_TRACE_DATA_COLLECTION = "ai.trace.data.collection.and.use.policy";
private static final String CONSENT_ID_TRACE_DATA_COM_COLLECTION = "ai.trace.data.collection.and.use.com.policy";
private static final String GROUP_CONSENT_ID_DATA_COLLECTION = "data.collection";
private static void setupLicensingFacade(char customerAgreementOnDetailedDataSharing) {
@@ -52,7 +52,7 @@ public class DataCollectionConsentUiTest extends BasePlatformTestCase {
public void testGroupForcedStateDescription() {
Consent fus = new Consent(CONSENT_ID_USAGE_STATS, Version.fromString("1.1"), "Send anonymous usage statistics", "text", false, false, "en");
Consent trace = new Consent(CONSENT_ID_TRACE_DATA_COLLECTION, Version.fromString("1.0"), "Send detailed code-related data", "text", false, false, "en");
Consent trace = new Consent(CONSENT_ID_TRACE_DATA_COM_COLLECTION, Version.fromString("1.0"), "Send detailed code-related data", "text", false, false, "en");
ConsentGroup group = new ConsentGroup(GROUP_CONSENT_ID_DATA_COLLECTION, List.of(fus, trace));
setupLicensingFacade('X');
@@ -89,7 +89,7 @@ public class DataCollectionConsentUiTest extends BasePlatformTestCase {
}
public void testTraceDataCollectionConsentForcedStateDependsOnAgreement() {
Consent trace = new Consent(CONSENT_ID_TRACE_DATA_COLLECTION, Version.fromString("1.0"), "Send detailed code-related data", "text", false,
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);