mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
OPENIDE fix statistic
This commit is contained in:
+6
-1
@@ -1,4 +1,7 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
//
|
||||
// Modified by Nikita Iarychenko at 2026 as part of the OpenIDE project (https://openide.ru).
|
||||
// Any modifications are available on the same license terms as the original source code.
|
||||
package com.intellij.internal.statistic.eventLog.validator.storage
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
@@ -57,6 +60,7 @@ import com.jetbrains.fus.reporting.model.serialization.SerializationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import ru.openide.statistics.OpenIdeFusHttpClient
|
||||
import tools.jackson.core.JsonGenerator
|
||||
import tools.jackson.core.StreamReadFeature
|
||||
import tools.jackson.core.util.DefaultIndenter
|
||||
@@ -232,7 +236,8 @@ object FusComponentProvider {
|
||||
|
||||
val jsonSerializer = FusJacksonSerializer()
|
||||
|
||||
val httpClient = applicationInfo.connectionSettings.createJvmHttpClient()
|
||||
// OpenIDE: the SDK hardcodes the JetBrains configuration URL, see OpenIdeFusHttpClient.
|
||||
val httpClient = OpenIdeFusHttpClient(applicationInfo.connectionSettings.createJvmHttpClient())
|
||||
|
||||
val remoteConfig = DefaultRemoteConfig(
|
||||
config,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// OpenIDE Project
|
||||
// Copyright (C) 2026 “Open Development Platform” Ltd. (https://openide.ru)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3 or later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see http://www.gnu.org/licenses/.
|
||||
package ru.openide.statistics
|
||||
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.jetbrains.fus.reporting.FusHttpClient
|
||||
import com.jetbrains.fus.reporting.HttpResponse
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
|
||||
/**
|
||||
* OpenIDE customization: keeps the bundled `com.jetbrains.fus.reporting` SDK away from the JetBrains
|
||||
* statistics services.
|
||||
*
|
||||
* The SDK composes its recorder-configuration URL itself, from the template hardcoded in
|
||||
* `com.jetbrains.fus.reporting.configuration.RegionCode`
|
||||
* (`https://resources.jetbrains.com/storage/fus/config/v4/<recorder>/<product>.json`), so the rebranded
|
||||
* [com.intellij.internal.statistic.eventLog.connection.ConfigurationClientFactory] — which only the legacy
|
||||
* upload path goes through — does not apply to it. Without this wrapper the SDK path
|
||||
* ([com.intellij.internal.statistic.eventLog.validator.storage.FusComponentProvider]) fetched the
|
||||
* configuration, the metadata and the dictionaries from `resources.jetbrains.com` for every recorder
|
||||
* (FUS, MP, ML), disclosing the product name, the build number and the client address to JetBrains on
|
||||
* every start.
|
||||
*
|
||||
* No event ever left through that path: the configuration served by JetBrains points `send` at
|
||||
* `analytics.services.jetbrains.com`, but nothing reads `RemoteConfig.getSendUrl()` — the uploader takes
|
||||
* its endpoint from the already rebranded legacy path instead. The refusal branch below is therefore a
|
||||
* guard against that changing, not a plug for an observed leak of collected data.
|
||||
*
|
||||
* `ru.openide.io.BlackListUrls` does not cover this: it is consulted only from
|
||||
* `com.intellij.util.io.HttpRequests`, while the SDK talks over `java.net.http.HttpClient`.
|
||||
*
|
||||
* Every request the SDK makes goes through [FusHttpClient], which is why this is the one place worth
|
||||
* wrapping. Hosts in [REWRITTEN_HOSTS] are replaced with [OPENIDE_STATISTICS_HOST], which mirrors the
|
||||
* JetBrains URL layout for all recorders, so only the host changes and the path is kept as-is. Any other
|
||||
* JetBrains host is refused rather than rewritten: the paths there do not match ours, and a request that
|
||||
* reaches this branch means a code path is still trying to report upstream, which should fail loudly in
|
||||
* the log instead of being silently repointed at a URL that does not exist.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
class OpenIdeFusHttpClient(private val delegate: FusHttpClient) : FusHttpClient {
|
||||
override fun post(url: String, data: String): HttpResponse {
|
||||
val target = toOpenIdeUrl(url) ?: return REFUSED
|
||||
return delegate.post(target, data)
|
||||
}
|
||||
|
||||
override fun get(url: String): HttpResponse {
|
||||
val target = toOpenIdeUrl(url) ?: return REFUSED
|
||||
return delegate.get(target)
|
||||
}
|
||||
|
||||
override fun lastModified(url: String): Long {
|
||||
val target = toOpenIdeUrl(url) ?: return 0
|
||||
return delegate.lastModified(target)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL to request, or `null` if it must not be requested at all.
|
||||
*/
|
||||
private fun toOpenIdeUrl(url: String): String? {
|
||||
val host = hostOf(url) ?: return url
|
||||
for (rewritten in REWRITTEN_HOSTS) {
|
||||
if (host.equals(rewritten, ignoreCase = true)) {
|
||||
return url.replaceFirst(host, OPENIDE_STATISTICS_HOST)
|
||||
}
|
||||
}
|
||||
if (host.equals(JETBRAINS_DOMAIN, ignoreCase = true) || host.endsWith(".$JETBRAINS_DOMAIN", ignoreCase = true)) {
|
||||
LOG.warn("Refused a statistics request to a JetBrains host: $url")
|
||||
return null
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
private fun hostOf(url: String): String? {
|
||||
val schemeEnd = url.indexOf("://")
|
||||
if (schemeEnd < 0) return null
|
||||
val hostStart = schemeEnd + "://".length
|
||||
val delimiter = url.indexOfAny(HOST_DELIMITERS, hostStart)
|
||||
val hostEnd = if (delimiter < 0) url.length else delimiter
|
||||
return url.substring(hostStart, hostEnd).ifEmpty { null }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val LOG = logger<OpenIdeFusHttpClient>()
|
||||
|
||||
private const val OPENIDE_STATISTICS_HOST = "stats.openide.ru"
|
||||
private const val JETBRAINS_DOMAIN = "jetbrains.com"
|
||||
|
||||
private val HOST_DELIMITERS = charArrayOf('/', ':', '?', '#')
|
||||
|
||||
/** Hosts whose URL layout `stats.openide.ru` mirrors, so that replacing the host is enough. */
|
||||
private val REWRITTEN_HOSTS = listOf(
|
||||
"resources.jetbrains.com",
|
||||
"resources.jetbrains.com.cn",
|
||||
)
|
||||
|
||||
/** What a refused request looks like to the SDK: a failed response it already knows how to handle. */
|
||||
private val REFUSED = HttpResponse(statusCode = 403, body = null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// OpenIDE Project
|
||||
// Copyright (C) 2026 “Open Development Platform” Ltd. (https://openide.ru)
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3 or later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see http://www.gnu.org/licenses/.
|
||||
package ru.openide.statistics
|
||||
|
||||
import com.jetbrains.fus.reporting.FusHttpClient
|
||||
import com.jetbrains.fus.reporting.HttpResponse
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class OpenIdeFusHttpClientTest {
|
||||
/** Records the URL the wrapper actually asked for, so that a refused request is visible as `null`. */
|
||||
private class RecordingClient : FusHttpClient {
|
||||
var requestedUrl: String? = null
|
||||
|
||||
override fun post(url: String, data: String): HttpResponse {
|
||||
requestedUrl = url
|
||||
return HttpResponse(200, "")
|
||||
}
|
||||
|
||||
override fun get(url: String): HttpResponse {
|
||||
requestedUrl = url
|
||||
return HttpResponse(200, "")
|
||||
}
|
||||
|
||||
override fun lastModified(url: String): Long {
|
||||
requestedUrl = url
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
private fun get(url: String): Pair<String?, HttpResponse> {
|
||||
val delegate = RecordingClient()
|
||||
val response = OpenIdeFusHttpClient(delegate).get(url)
|
||||
return delegate.requestedUrl to response
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `configuration url is taken from the OpenIDE host`() {
|
||||
val (requested, response) = get("https://resources.jetbrains.com/storage/fus/config/v4/FUS/IC.json")
|
||||
assertThat(requested).isEqualTo("https://stats.openide.ru/storage/fus/config/v4/FUS/IC.json")
|
||||
assertThat(response.statusCode).isEqualTo(200)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `china configuration host is rewritten as well`() {
|
||||
val (requested, _) = get("https://resources.jetbrains.com.cn/storage/fus/config/v4/ML/IC.json")
|
||||
assertThat(requested).isEqualTo("https://stats.openide.ru/storage/fus/config/v4/ML/IC.json")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `metadata and dictionary paths are kept as-is`() {
|
||||
val (requested, _) = get("https://resources.jetbrains.com/storage/ap/fus/metadata/dictionaries/FUS/dictionaries.json")
|
||||
assertThat(requested).isEqualTo("https://stats.openide.ru/storage/ap/fus/metadata/dictionaries/FUS/dictionaries.json")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reporting to the JetBrains analytics service is refused`() {
|
||||
val (requested, response) = get("https://analytics.services.jetbrains.com/fus/v5/send/")
|
||||
assertThat(requested).isNull()
|
||||
assertThat(response.statusCode).isEqualTo(403)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `any other JetBrains host is refused`() {
|
||||
val (requested, response) = get("https://plugins.jetbrains.com/api/search")
|
||||
assertThat(requested).isNull()
|
||||
assertThat(response.statusCode).isEqualTo(403)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refused requests report an unknown last modified timestamp`() {
|
||||
val delegate = RecordingClient()
|
||||
val lastModified = OpenIdeFusHttpClient(delegate).lastModified("https://uploads.jetbrains.com/x.json")
|
||||
assertThat(delegate.requestedUrl).isNull()
|
||||
assertThat(lastModified).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a host that only looks like a JetBrains one is not treated as JetBrains`() {
|
||||
val url = "https://resources.jetbrains.com.example.org/storage/fus/config/v4/FUS/IC.json"
|
||||
val (requested, _) = get(url)
|
||||
assertThat(requested).isEqualTo(url)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OpenIDE hosts are passed through untouched`() {
|
||||
val (requested, _) = get("https://stats.openide.ru/statistics/fus/v5/send/")
|
||||
assertThat(requested).isEqualTo("https://stats.openide.ru/statistics/fus/v5/send/")
|
||||
}
|
||||
}
|
||||
+11
-4
@@ -1,9 +1,11 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
//
|
||||
// Modified by Nikita Iarychenko at 2026 as part of the OpenIDE project (https://openide.ru).
|
||||
// Any modifications are available on the same license terms as the original source code.
|
||||
package com.intellij.stats.completion.sender
|
||||
|
||||
import com.intellij.ide.ApplicationActivity
|
||||
import com.intellij.internal.statistic.utils.StatisticsUploadAssistant
|
||||
import com.intellij.openapi.application.ApplicationInfo
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.components.serviceAsync
|
||||
@@ -35,9 +37,14 @@ internal class SenderPreloadingActivity : ApplicationActivity {
|
||||
serviceAsync<RegistryManager>()
|
||||
if (!Registry.`is`("completion.stats.analytics.platform.send", false)) return
|
||||
val urlValue = Registry.get("completion.stats.analytics.platform.url")
|
||||
val statusUrl =
|
||||
if (urlValue.isChangedFromDefault()) urlValue.asString()
|
||||
else "https://resources.jetbrains.com/storage/ap/mlcc/config/v1/${ApplicationInfo.getInstance().build.productCode}.json"
|
||||
// OpenIDE: upstream falls back to https://resources.jetbrains.com/storage/ap/mlcc/config/v1/<product>.json here.
|
||||
// OpenIDE has no such endpoint and completion logs must not be reported to JetBrains, so the sender stays off
|
||||
// unless an endpoint is configured explicitly through the registry key.
|
||||
if (!urlValue.isChangedFromDefault()) {
|
||||
LOG.info("Completion log sending is disabled: 'completion.stats.analytics.platform.url' is not configured.")
|
||||
return
|
||||
}
|
||||
val statusUrl = urlValue.asString()
|
||||
|
||||
// do not check right after the start - avoid getting UsageStatisticsPersistenceComponent too early
|
||||
delay(5.minutes)
|
||||
|
||||
Reference in New Issue
Block a user