diff --git a/platform/configuration-store-impl/src/ComponentStoreImpl.kt b/platform/configuration-store-impl/src/ComponentStoreImpl.kt index 439bb559b91d..a9d5c4c06719 100644 --- a/platform/configuration-store-impl/src/ComponentStoreImpl.kt +++ b/platform/configuration-store-impl/src/ComponentStoreImpl.kt @@ -202,7 +202,7 @@ abstract class ComponentStoreImpl : IComponentStore { continue } } - + var modificationCountChanged = false if (info.isModificationTrackingSupported) { currentModificationCount = info.currentModificationCount if (currentModificationCount == info.lastModificationCount) { @@ -211,9 +211,12 @@ abstract class ComponentStoreImpl : IComponentStore { continue } } + else { + modificationCountChanged = true + } } - commitComponent(session, info, name) + commitComponent(session, info, name, modificationCountChanged) info.updateModificationCount(currentModificationCount) } catch (e: Throwable) { @@ -243,7 +246,7 @@ abstract class ComponentStoreImpl : IComponentStore { val stateSpec = getStateSpec(component) LOG.debug { "saveComponent is called for ${stateSpec.name}" } val saveManager = createSaveSessionProducerManager() - commitComponent(saveManager, ComponentInfoImpl(component, stateSpec), null) + commitComponent(saveManager, ComponentInfoImpl(component, stateSpec), null, false) val absolutePath = Paths.get(storageManager.expandMacros(findNonDeprecated(getStorageSpecs(component, stateSpec, StateStorageOperation.WRITE)).path)).toAbsolutePath().toString() Disposer.newDisposable().use { VfsRootAccess.allowRootAccess(it, absolutePath) @@ -260,7 +263,10 @@ abstract class ComponentStoreImpl : IComponentStore { open fun createSaveSessionProducerManager() = SaveSessionProducerManager() - private fun commitComponent(session: SaveSessionProducerManager, info: ComponentInfo, componentName: String?) { + private fun commitComponent(session: SaveSessionProducerManager, + info: ComponentInfo, + componentName: String?, + modificationCountChanged: Boolean) { val component = info.component @Suppress("DEPRECATION") if (component is com.intellij.openapi.util.JDOMExternalizable) { @@ -306,6 +312,12 @@ abstract class ComponentStoreImpl : IComponentStore { state = (info.component as PersistentStateComponent<*>).state } + if (modificationCountChanged && stateSpec.reportStatistic && state != null) { + LOG.runAndLogException { + FeatureUsageSettingsEvents.logConfigurationChanged(effectiveComponentName, state, project) + } + } + setStateToSaveSessionProducer(state, info, effectiveComponentName, sessionProducer) } } diff --git a/platform/configuration-store-impl/src/statistic/eventLog/FeatureUsageSettingsEvents.kt b/platform/configuration-store-impl/src/statistic/eventLog/FeatureUsageSettingsEvents.kt index ba737bc5f27b..82cf5f1d01a4 100644 --- a/platform/configuration-store-impl/src/statistic/eventLog/FeatureUsageSettingsEvents.kt +++ b/platform/configuration-store-impl/src/statistic/eventLog/FeatureUsageSettingsEvents.kt @@ -3,23 +3,24 @@ package com.intellij.configurationStore.statistic.eventLog import com.intellij.configurationStore.jdomSerializer import com.intellij.internal.statistic.eventLog.EventLogGroup +import com.intellij.internal.statistic.eventLog.FeatureUsageData import com.intellij.internal.statistic.eventLog.fus.FeatureUsageLogger +import com.intellij.internal.statistic.service.fus.collectors.FUCounterUsageLogger import com.intellij.internal.statistic.utils.PluginInfo -import com.intellij.internal.statistic.utils.StatisticsUtil -import com.intellij.internal.statistic.utils.addPluginInfoTo import com.intellij.internal.statistic.utils.getPluginInfo import com.intellij.openapi.components.ReportValue import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project -import com.intellij.serialization.MutableAccessor import com.intellij.util.concurrency.NonUrgentExecutor import com.intellij.util.containers.ContainerUtil +import com.intellij.util.xmlb.Accessor import com.intellij.util.xmlb.BeanBinding import org.jdom.Element -import java.util.* +import java.util.concurrent.atomic.AtomicInteger private val LOG = Logger.getInstance("com.intellij.configurationStore.statistic.eventLog.FeatureUsageSettingsEventPrinter") -private val GROUP = EventLogGroup("settings", 5) +private val GROUP = EventLogGroup("settings", 6) +private const val CHANGES_GROUP = "settings.changes" private val recordedComponents: MutableSet = ContainerUtil.newConcurrentSet() private val recordedOptionNames: MutableSet = ContainerUtil.newConcurrentSet() @@ -50,20 +51,31 @@ internal object FeatureUsageSettingsEvents { } } } + + fun logConfigurationChanged(componentName: String, state: Any, project: Project?) { + NonUrgentExecutor.getInstance().execute { + if (FeatureUsageLogger.isEnabled()) { + printer.logConfigurationStateChanged(componentName, state, project) + } + } + } } open class FeatureUsageSettingsEventPrinter(private val recordDefault: Boolean) { + private val valuesExtractor = ConfigurationStateExtractor(recordDefault) + fun logDefaultConfigurationState(componentName: String, clazz: Class<*>, project: Project?) { try { if (recordDefault) { val default = jdomSerializer.getDefaultSerializationFilter().getDefaultValue(clazz) logConfigurationState(componentName, default, project) } - else if (clazz != Element::class.java && getPluginInfo(clazz).isDevelopedByJetBrains()) { - recordedComponents.add(componentName) - val isDefaultProject = project?.isDefault == true - val hash = if (!isDefaultProject) toHash(project) else null - logSettingCollectorWasInvoked(componentName, isDefaultProject, hash) + else if (clazz != Element::class.java) { + val pluginInfo = getPluginInfo(clazz) + if (pluginInfo.isDevelopedByJetBrains()) { + recordedComponents.add(componentName) + logConfig(GROUP, "invoked", createComponentData(project, componentName, pluginInfo)) + } } } catch (e: Exception) { @@ -71,118 +83,148 @@ open class FeatureUsageSettingsEventPrinter(private val recordDefault: Boolean) } } + fun logConfigurationStateChanged(componentName: String, state: Any?, project: Project?) { + val (optionsValues, pluginInfo) = valuesExtractor.extract(project, componentName, state) ?: return + val id = counter.incrementAndGet() + for (data in optionsValues) { + logSettingsChanged("component_changed_option", data, id) + } + + if (!recordDefault) { + logSettingsChanged("component_changed", createComponentData(project, componentName, pluginInfo), id) + } + } + fun logConfigurationState(componentName: String, state: Any?, project: Project?) { + val (optionsValues, pluginInfo) = valuesExtractor.extract(project, componentName, state) ?: return + val eventId = if (recordDefault) "option" else "not.default" + for (data in optionsValues) { + logConfig(GROUP, eventId, data) + } + + if (!recordDefault) { + logConfig(GROUP, "invoked", createComponentData(project, componentName, pluginInfo)) + } + } + + protected open fun logConfig(group: EventLogGroup, eventId: String, data: FeatureUsageData) { + FeatureUsageLogger.logState(group, eventId, data.build()) + } + + protected open fun logSettingsChanged(eventId: String, data: FeatureUsageData, id: Int) { + FUCounterUsageLogger.getInstance().logEvent(CHANGES_GROUP, eventId, data.addData("id", id)) + } + + companion object { + private val counter = AtomicInteger(0) + + fun createComponentData(project: Project?, componentName: String, pluginInfo: PluginInfo): FeatureUsageData { + val data = FeatureUsageData() + .addData("component", componentName) + .addPluginInfo(pluginInfo) + if (project?.isDefault == true) { + data.addData("default_project", true) + } + else { + data.addProject(project) + } + return data + } + } +} + +internal data class ConfigurationState(val optionsValues: List, val pluginInfo: PluginInfo) + +internal data class ConfigurationStateExtractor(val recordDefault: Boolean) { + internal fun extract(project: Project?, componentName: String, state: Any?): ConfigurationState? { if (state == null || state is Element) { - return + return null } val pluginInfo = getPluginInfo(state.javaClass) if (!pluginInfo.isDevelopedByJetBrains()) { - return + return null } val accessors = BeanBinding.getAccessors(state.javaClass) if (accessors.isEmpty()) { - return + return null } recordedComponents.add(componentName) - val eventId = if (recordDefault) "option" else "not.default" - val isDefaultProject = project?.isDefault == true - val hash = if (!isDefaultProject) toHash(project) else null - - for (accessor in accessors) { - val type = accessor.genericType - if (type === Boolean::class.javaPrimitiveType) { - logConfigValue(accessor, state, "bool", eventId, isDefaultProject, true, hash, componentName, pluginInfo) - } - else if (type === Int::class.javaPrimitiveType || type === Long::class.javaPrimitiveType) { - logConfigValue(accessor, state, "int", eventId, isDefaultProject, shouldReportValue(accessor), hash, componentName, pluginInfo) - } - else if (type === Float::class.javaPrimitiveType || type === Double::class.javaPrimitiveType) { - logConfigValue(accessor, state, "float", eventId, isDefaultProject, shouldReportValue(accessor), hash, componentName, pluginInfo) - } - else if (type is Class<*> && type.isEnum) { - logConfigValue(accessor, state, "enum", eventId, isDefaultProject, shouldReportValue(accessor), hash, componentName, pluginInfo) { - (it as? Enum<*>)?.name - } - } - else if (type == String::class.java) { - val annotation = accessor.getAnnotation(ReportValue::class.java) - logConfigValue(accessor, state, "string", eventId, isDefaultProject, shouldReportValue(accessor), hash, componentName, pluginInfo) { - if (it in annotation.possibleValues) it else null - } - } - } - - if (!recordDefault) { - logSettingCollectorWasInvoked(componentName, isDefaultProject, hash) - } + val optionsValues = accessors.mapNotNull { extractOptionValue(project, it, state, componentName, pluginInfo) } + return ConfigurationState(optionsValues, pluginInfo) } - private fun shouldReportValue(accessor: MutableAccessor): Boolean = accessor.getAnnotation(ReportValue::class.java) != null - - private fun logConfigValue(accessor: MutableAccessor, - state: Any, - type: String, - eventId: String, - isDefaultProject: Boolean, - reportValue: Boolean, - hash: String?, - componentName: String, - pluginInfo: PluginInfo, - transformValue: ((Any?) -> Any?)? = null) { + private fun extractOptionValue(project: Project?, + accessor: Accessor, + state: Any, + componentName: String, + pluginInfo: PluginInfo): FeatureUsageData? { val isDefault = !jdomSerializer.getDefaultSerializationFilter().accepts(accessor, state) if (!isDefault || recordDefault) { + val data = FeatureUsageSettingsEventPrinter.createComponentData(project, componentName, pluginInfo) recordedOptionNames.add(accessor.name) - val content = HashMap() - content["type"] = type - content["component"] = componentName - content["name"] = accessor.name - if (reportValue) { - val value = accessor.readUnsafe(state) - val transformedValue = if (transformValue != null) transformValue(value) else value - if (transformedValue != null) { - content["value"] = transformedValue + data.addData("name", accessor.name) + if (tryAddTypedValue(data, accessor, state)) { + if (recordDefault) { + data.addData("default", isDefault) } - } - if (recordDefault) { - content["default"] = isDefault - } - addProjectOptions(content, isDefaultProject, hash) - addPluginInfoTo(pluginInfo, content) - logConfig(GROUP, eventId, content) - } - } - - private fun addProjectOptions(content: HashMap, - isDefaultProject: Boolean, - projectHash: String?) { - if (isDefaultProject) { - content["default_project"] = true - } - else { - projectHash?.let { - content["project"] = projectHash + return data } } + return null } - @Suppress("SameParameterValue") - protected open fun logConfig(group: EventLogGroup, eventId: String, data: Map) { - FeatureUsageLogger.logState(group, eventId, data) + private fun tryAddTypedValue(data: FeatureUsageData, accessor: Accessor, state: Any): Boolean { + val type = accessor.genericType + when { + type === Boolean::class.javaPrimitiveType -> { + data.addData("type", "bool") + val value = accessor.readUnsafe(state) as? Boolean + value?.let { data.addData("value", it) } + } + type === Int::class.javaPrimitiveType -> addValue(data, accessor, state, "int") { data.addData("value", it) } + type === Long::class.javaPrimitiveType -> addValue(data, accessor, state, "int") { data.addData("value", it) } + type === Float::class.javaPrimitiveType -> addValue(data, accessor, state, "float") { data.addData("value", it) } + type === Double::class.javaPrimitiveType -> addValue(data, accessor, state, "float") { data.addData("value", it) } + type is Class<*> && type.isEnum -> { + data.addData("type", "enum") + readValue(accessor, state) { (it as? Enum<*>)?.name }?.let { data.addData("value", it) } + } + type == String::class.java -> { + data.addData("type", "string") + val value = readValue(accessor, state) { value -> + if (value is String && value in accessor.getAnnotation(ReportValue::class.java).possibleValues) { + value + } + else null + } + value?.let { data.addData("value", it) } + } + else -> return false + } + return true } - private fun logSettingCollectorWasInvoked(componentName: String, isDefaultProject: Boolean, projectHash: String?) { - val content = HashMap() - content["component"] = componentName - addProjectOptions(content, isDefaultProject, projectHash) - logConfig(GROUP, "invoked", content) + private inline fun readValue(accessor: Accessor, state: Any, noinline transformValue: ((Any?) -> T?)? = null): T? { + if (accessor.getAnnotation(ReportValue::class.java) != null) { + val value = accessor.readUnsafe(state) + return if (transformValue != null) { + transformValue(value) + } + else { + value as? T + } + } + return null } - internal fun toHash(project: Project?): String? { - return project?.let { - return StatisticsUtil.getProjectId(project) + private inline fun addValue(data: FeatureUsageData, accessor: Accessor, state: Any, type: String, add: (T) -> Unit) { + data.addData("type", type) + val value = readValue(accessor, state) + if (value != null) { + add(value) } } } diff --git a/platform/configuration-store-impl/testSrc/statistics/eventLog/FeatureUsageSettingsEventsTest.kt b/platform/configuration-store-impl/testSrc/statistics/eventLog/FeatureUsageSettingsEventsTest.kt index 9a98fd8d5722..bf3ef2c08863 100644 --- a/platform/configuration-store-impl/testSrc/statistics/eventLog/FeatureUsageSettingsEventsTest.kt +++ b/platform/configuration-store-impl/testSrc/statistics/eventLog/FeatureUsageSettingsEventsTest.kt @@ -1,9 +1,10 @@ -// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.configurationStore.statistics.eventLog import com.intellij.configurationStore.getStateSpec import com.intellij.configurationStore.statistic.eventLog.FeatureUsageSettingsEventPrinter import com.intellij.internal.statistic.eventLog.EventLogGroup +import com.intellij.internal.statistic.eventLog.FeatureUsageData import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.ReportValue import com.intellij.openapi.components.State @@ -23,18 +24,6 @@ class FeatureUsageSettingsEventsTest { val projectRule = ProjectRule() } - @Test - fun `project name to hash`() { - val printer = TestFeatureUsageSettingsEventsPrinter(true) - assertThat(printer.toHash(projectRule.project)).isNotNull - } - - @Test - fun `no project name to hash`() { - val printer = TestFeatureUsageSettingsEventsPrinter(true) - assertThat(printer.toHash(null)).isNull() - } - @Test fun `record all default application component with enabled default recording`() { val component = TestComponent() @@ -402,6 +391,54 @@ class FeatureUsageSettingsEventsTest { "string", recordDefault, withProject, defaultProject) } + @Test + fun `same project hash in invoke and state action`() { + val component = TestComponent() + component.loadState(ComponentState(bool = true)) + val printer = TestFeatureUsageSettingsEventsPrinter(false) + printer.logConfigurationState(getStateSpec(component).name, component.state, null) + assertThat(printer.getInvokedEvent().data["project"]).isEqualTo(printer.getOptionByName("boolOption").data["project"]) + } + + @Test + fun `log changed to default setting`() { + val component = TestComponent() + component.loadState(MultiComponentState()) + val printer = TestFeatureUsageSettingsChangedPrinter(false) + val state = getStateSpec(component) + printer.logConfigurationStateChanged(state.name, component.state, projectRule.project) + + assertThat(printer.result).hasSize(1) + validateChangedComponent(state, printer.result.first()) + } + + private fun validateChangedComponent(state: State, event: Pair) { + val (eventId, usageData) = event + assertThat(eventId).isEqualTo("component_changed") + val data = usageData.build() + assertThat(data["component"]).isEqualTo(state.name) + assertThat(data).containsOnlyKeys("component", "plugin_type", "project") + } + + @Test + fun `log changed to not default settings`() { + val component = TestComponent() + component.loadState(MultiComponentState(secondBool = false)) + val printer = TestFeatureUsageSettingsChangedPrinter(false) + val state = getStateSpec(component) + printer.logConfigurationStateChanged(state.name, component.state, projectRule.project) + + assertThat(printer.result).hasSize(2) + val (eventId, usageData) = printer.result[0] + assertThat(eventId).isEqualTo("component_changed_option") + val data = usageData.build() + assertThat(data["component"]).isEqualTo(state.name) + assertThat(data["type"]).isEqualTo("bool") + assertThat(data["name"]).isEqualTo("secondBoolOption") + assertThat(data).containsOnlyKeys("component", "plugin_type", "project", "type", "name", "value") + validateChangedComponent(state, printer.result[1]) + } + private fun assertDefaultWithoutDefaultRecording(printer: TestFeatureUsageSettingsEventsPrinter, withProject: Boolean, defaultProject: Boolean) { @@ -499,9 +536,13 @@ class FeatureUsageSettingsEventsTest { var size = 1 if (withProject) size++ if (defaultProject) size++ + if (event.data.containsKey("plugin_type")) size++ + if (event.data.containsKey("plugin_version")) size++ + if (event.data.containsKey("plugin")) size++ assertThat(event.data).hasSize(size) assertThat(event.data["component"]).isEqualTo("MyTestComponent") + assertThat(event.data["plugin_type"]).isNotNull if (withProject) { assertThat(event.data).containsKey("project") } @@ -513,8 +554,8 @@ class FeatureUsageSettingsEventsTest { private class TestFeatureUsageSettingsEventsPrinter(recordDefault: Boolean) : FeatureUsageSettingsEventPrinter(recordDefault) { val result: MutableList = ArrayList() - override fun logConfig(group: EventLogGroup, eventId: String, data: Map) { - result.add(LoggedComponentStateEvents(group, eventId, data)) + override fun logConfig(group: EventLogGroup, eventId: String, data: FeatureUsageData) { + result.add(LoggedComponentStateEvents(group, eventId, data.build())) } fun getOptionByName(name: String): LoggedComponentStateEvents { @@ -536,6 +577,14 @@ class FeatureUsageSettingsEventsTest { } } + private class TestFeatureUsageSettingsChangedPrinter(recordDefault: Boolean) : FeatureUsageSettingsEventPrinter(recordDefault) { + val result: MutableList> = ArrayList() + + override fun logSettingsChanged(eventId: String, data: FeatureUsageData, id: Int) { + result.add(Pair(eventId, data)) + } + } + private class LoggedComponentStateEvents(val group: EventLogGroup, val id: String, val data: Map) @State(name = "MyTestComponent", reportStatistic = true) @@ -608,6 +657,7 @@ class FeatureUsageSettingsEventsTest { val absDoubleOption: Double = absDoubleOpt } + @Suppress("unused") private class ComponentStateWithEnum(enumOpt: EnumOption = EnumOption.FOO, absEnumOpt: EnumOption = EnumOption.FOO) : ComponentState() { @Attribute("enum-option") @@ -622,6 +672,7 @@ class FeatureUsageSettingsEventsTest { } } + @Suppress("unused") private class ComponentStateWithString(stringOpt: String = "test", absStringOpt: String = "test", absStringOptWithoutPossibleValues: String = "test") : ComponentState() { diff --git a/platform/statistics/src/com/intellij/internal/statistic/eventLog/fus/FeatureUsageEventLoggerProvider.kt b/platform/statistics/src/com/intellij/internal/statistic/eventLog/fus/FeatureUsageEventLoggerProvider.kt index e58b7435fb47..af391baf77f2 100644 --- a/platform/statistics/src/com/intellij/internal/statistic/eventLog/fus/FeatureUsageEventLoggerProvider.kt +++ b/platform/statistics/src/com/intellij/internal/statistic/eventLog/fus/FeatureUsageEventLoggerProvider.kt @@ -6,7 +6,7 @@ import com.intellij.internal.statistic.utils.StatisticsUploadAssistant import com.intellij.openapi.application.ApplicationManager import java.util.concurrent.TimeUnit -internal class FeatureUsageEventLoggerProvider : StatisticsEventLoggerProvider("FUS", 46, sendFrequencyMs = TimeUnit.MINUTES.toMillis(15)) { +internal class FeatureUsageEventLoggerProvider : StatisticsEventLoggerProvider("FUS", 47, sendFrequencyMs = TimeUnit.MINUTES.toMillis(15)) { override fun isRecordEnabled(): Boolean { return !ApplicationManager.getApplication().isHeadlessEnvironment && StatisticsUploadAssistant.isCollectAllowed() diff --git a/platform/statistics/src/com/intellij/internal/statistic/service/fus/collectors/FUCounterUsageLogger.java b/platform/statistics/src/com/intellij/internal/statistic/service/fus/collectors/FUCounterUsageLogger.java index 3fdcd01f78b9..d9c986a231ad 100644 --- a/platform/statistics/src/com/intellij/internal/statistic/service/fus/collectors/FUCounterUsageLogger.java +++ b/platform/statistics/src/com/intellij/internal/statistic/service/fus/collectors/FUCounterUsageLogger.java @@ -66,7 +66,7 @@ public class FUCounterUsageLogger { private static final String[] GENERAL_GROUPS = new String[]{ "event.log", "performance", "ui.dialogs", "ui.settings", "toolwindow", "intentions", "run.configuration.exec", - "productivity", "completion.postfix", "notifications" + "productivity", "completion.postfix", "notifications", "settings.changes" }; private static final Logger LOG = Logger.getInstance(FUCounterUsageLogger.class);