Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import org.sonarsource.sonarlint.core.serverconnection.OrganizationSynchronizer;
import org.sonarsource.sonarlint.core.serverconnection.ServerInfoSynchronizer;
import org.sonarsource.sonarlint.core.serverconnection.SonarServerSettingsChangedEvent;
import org.sonarsource.sonarlint.core.serverconnection.UserSynchronizer;
import org.sonarsource.sonarlint.core.storage.StorageService;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
Expand Down Expand Up @@ -319,8 +320,10 @@ private void synchronizeConnectionAndProjectsIfNeededSync(String connectionId, S
var serverInfoSynchronizer = new ServerInfoSynchronizer(storage);
var storageSynchronizer = new LocalStorageSynchronizer(enabledLanguagesToSync, connectedModeEmbeddedPluginKeys, serverInfoSynchronizer, storage);
var aiCodeFixSynchronizer = new AiCodeFixSettingsSynchronizer(storage, new OrganizationSynchronizer(storage));
var userSynchronizer = new UserSynchronizer(storage);
try {
LOG.debug("Synchronizing storage of connection '{}'", connectionId);
userSynchronizer.synchronize(serverApi, cancelMonitor);
var summary = storageSynchronizer.synchronizeServerInfosAndPlugins(serverApi, cancelMonitor);
if (summary.anyPluginSynchronized()) {
applicationEventPublisher.publishEvent(new PluginsSynchronizedEvent(connectionId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
import org.sonarsource.sonarlint.core.repository.connection.SonarCloudConnectionConfiguration;
import org.sonarsource.sonarlint.core.repository.connection.SonarQubeConnectionConfiguration;
import org.sonarsource.sonarlint.core.repository.rules.RulesRepository;
import org.sonarsource.sonarlint.core.rules.RulesService;
import org.sonarsource.sonarlint.core.serverconnection.Organization;
import org.sonarsource.sonarlint.core.serverconnection.StoredServerInfo;
import org.sonarsource.sonarlint.core.storage.StorageService;

public class TelemetryServerAttributesProvider {

Expand All @@ -42,15 +44,17 @@ public class TelemetryServerAttributesProvider {
private final ActiveRulesService activeRulesService;
private final RulesRepository rulesRepository;
private final NodeJsService nodeJsService;
private final StorageService storageService;

public TelemetryServerAttributesProvider(ConfigurationRepository configurationRepository,
ConnectionConfigurationRepository connectionConfigurationRepository,
ActiveRulesService activeRulesService, RulesRepository rulesRepository, NodeJsService nodeJsService) {
ConnectionConfigurationRepository connectionConfigurationRepository, ActiveRulesService activeRulesService, RulesRepository rulesRepository,
NodeJsService nodeJsService, StorageService storageService) {
this.configurationRepository = configurationRepository;
this.connectionConfigurationRepository = connectionConfigurationRepository;
this.activeRulesService = activeRulesService;
this.rulesRepository = rulesRepository;
this.nodeJsService = nodeJsService;
this.storageService = storageService;
}

public TelemetryServerAttributes getTelemetryServerLiveAttributes() {
Expand Down Expand Up @@ -85,9 +89,25 @@ public TelemetryServerAttributes getTelemetryServerLiveAttributes() {

var nodeJsVersion = getNodeJsVersion();

var connectionsAttributes = connectionConfigurationRepository.getConnectionsById().keySet().stream()
.map(storageService::connection)
.map(c -> {
var userId = c.user().read().orElse(null);
var serverId = c.serverInfo().read().map(StoredServerInfo::serverId).orElse(null);
var orgId = c.organization().read().map(Organization::id).orElse(null);

if (userId == null && serverId == null && orgId == null) {
return null;
}

return new TelemetryConnectionAttributes(userId, serverId, orgId);
})
.filter(Objects::nonNull)
.toList();

return new TelemetryServerAttributes(usesConnectedMode, usesSonarCloud, childBindingCount, sonarQubeServerBindingCount,
sonarQubeCloudEUBindingCount, sonarQubeCloudUSBindingCount, devNotificationsDisabled, nonDefaultEnabledRules,
defaultDisabledRules, nodeJsVersion);
defaultDisabledRules, nodeJsVersion, connectionsAttributes);
}

private int countSonarQubeCloudBindings(Collection<BoundScope> allBindings, SonarCloudRegion region) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.sonarsource.sonarlint.core.repository.rules.RulesRepository;
import org.sonarsource.sonarlint.core.rpc.protocol.backend.rules.StandaloneRuleConfigDto;
import org.sonarsource.sonarlint.core.rule.extractor.SonarLintRuleDefinition;
import org.sonarsource.sonarlint.core.storage.StorageService;
import org.sonarsource.sonarlint.core.telemetry.TelemetryServerAttributesProvider;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -56,7 +57,7 @@ void it_should_calculate_connectedMode_usesSC_notDisabledNotifications_telemetry

var connectionConfigurationRepository = mock(ConnectionConfigurationRepository.class);
when(connectionConfigurationRepository.getConnectionById(connectionId)).thenReturn(new SonarCloudConnectionConfiguration(SonarCloudRegion.EU.getProductionUri(), SonarCloudRegion.EU.getApiProductionUri(), connectionId, "myTestOrg", SonarCloudRegion.EU, false));
var underTest = new TelemetryServerAttributesProvider(configurationRepository, connectionConfigurationRepository, mock(ActiveRulesService.class), mock(RulesRepository.class), mock(NodeJsService.class));
var underTest = new TelemetryServerAttributesProvider(configurationRepository, connectionConfigurationRepository, mock(ActiveRulesService.class), mock(RulesRepository.class), mock(NodeJsService.class), mock(StorageService.class));

var telemetryLiveAttributes = underTest.getTelemetryServerLiveAttributes();
assertThat(telemetryLiveAttributes.usesConnectedMode()).isTrue();
Expand Down Expand Up @@ -97,7 +98,7 @@ void it_should_calculate_connectedMode_notUsesSC_disabledDevNotifications_teleme
var connectionConfigurationRepository = mock(ConnectionConfigurationRepository.class);
when(connectionConfigurationRepository.getConnectionById(connectionId_1)).thenReturn(new SonarQubeConnectionConfiguration(connectionId_1, "www.squrl1.org", false));
when(connectionConfigurationRepository.getConnectionById(connectionId_2)).thenReturn(new SonarQubeConnectionConfiguration(connectionId_2, "www.squrl2.org", true));
var underTest = new TelemetryServerAttributesProvider(configurationRepository, connectionConfigurationRepository, mock(ActiveRulesService.class), mock(RulesRepository.class), mock(NodeJsService.class));
var underTest = new TelemetryServerAttributesProvider(configurationRepository, connectionConfigurationRepository, mock(ActiveRulesService.class), mock(RulesRepository.class), mock(NodeJsService.class), mock(StorageService.class));

var telemetryLiveAttributes = underTest.getTelemetryServerLiveAttributes();
assertThat(telemetryLiveAttributes.usesConnectedMode()).isTrue();
Expand Down Expand Up @@ -130,7 +131,7 @@ void it_should_calculate_disabledRules_enabledRules_telemetry_attrs() {
when(rulesRepository.getEmbeddedRule("ruleKey_3")).thenReturn(sonarLintRuleDefinition_3);
when(rulesRepository.getEmbeddedRule("ruleKey_4")).thenReturn(sonarLintRuleDefinition_4);

var underTest = new TelemetryServerAttributesProvider(mock(ConfigurationRepository.class), mock(ConnectionConfigurationRepository.class), activeRulesService, rulesRepository, mock(NodeJsService.class));
var underTest = new TelemetryServerAttributesProvider(mock(ConfigurationRepository.class), mock(ConnectionConfigurationRepository.class), activeRulesService, rulesRepository, mock(NodeJsService.class), mock(StorageService.class));
var telemetryLiveAttributes = underTest.getTelemetryServerLiveAttributes();

assertThat(telemetryLiveAttributes.nonDefaultEnabledRules()).containsExactly("ruleKey_2");
Expand All @@ -149,7 +150,7 @@ void it_should_test_nodejs_version_telemetry_attr() {
var nodeJsService = mock(NodeJsService.class);
var version = "3.1.4.159";
when(nodeJsService.getActiveNodeJsVersion()).thenReturn(Optional.of(Version.create(version)));
var underTest = new TelemetryServerAttributesProvider(mock(ConfigurationRepository.class), mock(ConnectionConfigurationRepository.class), mock(ActiveRulesService.class), mock(RulesRepository.class), nodeJsService);
var underTest = new TelemetryServerAttributesProvider(mock(ConfigurationRepository.class), mock(ConnectionConfigurationRepository.class), mock(ActiveRulesService.class), mock(RulesRepository.class), nodeJsService, mock(StorageService.class));

assertThat(underTest.getTelemetryServerLiveAttributes().nodeVersion()).isEqualTo(version);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import org.sonarsource.sonarlint.core.serverapi.settings.SettingsApi;
import org.sonarsource.sonarlint.core.serverapi.source.SourceApi;
import org.sonarsource.sonarlint.core.serverapi.system.SystemApi;
import org.sonarsource.sonarlint.core.serverapi.users.UsersApi;

public class ServerApi {
private final ServerApiHelper helper;
Expand Down Expand Up @@ -128,6 +129,10 @@ public ScaApi sca() {
return new ScaApi(helper);
}

public UsersApi users() {
return new UsersApi(helper);
}

public boolean isSonarCloud() {
return helper.isSonarCloud();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* SonarLint Core - Server API
* Copyright (C) 2016-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.core.serverapi.users;

import com.google.gson.Gson;
import javax.annotation.CheckForNull;
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor;
import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper;

public class UsersApi {
private static final SonarLintLogger LOG = SonarLintLogger.get();

private final ServerApiHelper helper;

public UsersApi(ServerApiHelper helper) {
this.helper = helper;
}

/**
* Fetch the current user info on SonarQube Cloud (SQC) using api/users/current.
* Returns null on SonarQube Server or if the response cannot be parsed.
*/
@CheckForNull
public String getCurrentUserId(SonarLintCancelMonitor cancelMonitor) {
if (!helper.isSonarCloud()) {
return null;
}
try (var response = helper.get("/api/users/current", cancelMonitor)) {
var body = response.bodyAsString();
try {
var userResponse = new Gson().fromJson(body, CurrentUserResponse.class);
return userResponse == null ? null : userResponse.id;
} catch (Exception e) {
LOG.error("Error while parsing /api/users/current response", e);
return null;
}
}
}

private static class CurrentUserResponse {
String id;
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* SonarLint Core - Server API
* Copyright (C) 2016-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.core.serverapi.users;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogTester;
import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor;
import org.sonarsource.sonarlint.core.serverapi.MockWebServerExtensionWithProtobuf;
import org.sonarsource.sonarlint.core.serverapi.ServerApiHelper;

import static org.assertj.core.api.Assertions.assertThat;

class UsersApiTests {

@RegisterExtension
private static final SonarLintLogTester logTester = new SonarLintLogTester();

@RegisterExtension
static MockWebServerExtensionWithProtobuf mockServer = new MockWebServerExtensionWithProtobuf();

private UsersApi underTest;

@BeforeEach
void setUp() {
// default with SonarCloud organization to trigger api base URL and isSonarCloud = true
ServerApiHelper helper = mockServer.serverApiHelper("orgKey");
underTest = new UsersApi(helper);
}

@Test
void should_return_user_id_on_sonarcloud() {
mockServer.addStringResponse("/api/users/current", """
{
"isLoggedIn": true,
"id": "16c9b3b3-3f7e-4d61-91fe-31d731456c08",
"login": "obiwan.kenobi"
}""");

var id = underTest.getCurrentUserId(new SonarLintCancelMonitor());

assertThat(id).isEqualTo("16c9b3b3-3f7e-4d61-91fe-31d731456c08");
}

@Test
void should_return_null_on_sonarqube_server() {
var helperSqs = mockServer.serverApiHelper(null); // isSonarCloud = false
var api = new UsersApi(helperSqs);

var id = api.getCurrentUserId(new SonarLintCancelMonitor());

assertThat(id).isNull();
}

@Test
void should_return_null_on_malformed_response() {
mockServer.addStringResponse("/api/users/current", "{}");

var id = underTest.getCurrentUserId(new SonarLintCancelMonitor());

assertThat(id).isNull();
}

}


Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.sonarsource.sonarlint.core.serverconnection.storage.PluginsStorage;
import org.sonarsource.sonarlint.core.serverconnection.storage.ServerInfoStorage;
import org.sonarsource.sonarlint.core.serverconnection.storage.ServerIssueStoresManager;
import org.sonarsource.sonarlint.core.serverconnection.storage.UserStorage;

import static org.sonarsource.sonarlint.core.serverconnection.storage.ProjectStoragePaths.encodeForFs;

Expand All @@ -39,6 +40,7 @@ public class ConnectionStorage {
private final Path connectionStorageRoot;
private final AiCodeFixStorage aiCodeFixStorage;
private final OrganizationStorage organizationStorage;
private final UserStorage userStorage;

public ConnectionStorage(Path globalStorageRoot, Path workDir, String connectionId) {
this.connectionStorageRoot = globalStorageRoot.resolve(encodeForFs(connectionId));
Expand All @@ -48,6 +50,7 @@ public ConnectionStorage(Path globalStorageRoot, Path workDir, String connection
this.pluginsStorage = new PluginsStorage(connectionStorageRoot);
this.aiCodeFixStorage = new AiCodeFixStorage(connectionStorageRoot);
this.organizationStorage = new OrganizationStorage(connectionStorageRoot);
this.userStorage = new UserStorage(connectionStorageRoot);
}

public ServerInfoStorage serverInfo() {
Expand All @@ -71,6 +74,10 @@ public OrganizationStorage organization() {
return organizationStorage;
}

public UserStorage user() {
return userStorage;
}

public void close() {
serverIssueStoresManager.close();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

import static org.sonarsource.sonarlint.core.serverconnection.ServerSettings.MQR_MODE_SETTING;

public record StoredServerInfo(Version version, Set<Feature> features, ServerSettings globalSettings) {
public record StoredServerInfo(Version version, Set<Feature> features, ServerSettings globalSettings, String serverId) {
private static final String MIN_MQR_MODE_SUPPORT_VERSION = "10.2";
private static final String MQR_MODE_SETTING_MIN_VERSION = "10.8";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* SonarLint Core - Server Connection
* Copyright (C) 2016-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonarsource.sonarlint.core.serverconnection;

import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
import org.sonarsource.sonarlint.core.commons.progress.SonarLintCancelMonitor;
import org.sonarsource.sonarlint.core.serverapi.ServerApi;

public class UserSynchronizer {
private static final SonarLintLogger LOG = SonarLintLogger.get();
private final ConnectionStorage storage;

public UserSynchronizer(ConnectionStorage storage) {
this.storage = storage;
}

/**
* Fetches and stores the user id on SQC. On SQS, does nothing (file won't be created).
*/
public void synchronize(ServerApi serverApi, SonarLintCancelMonitor cancelMonitor) {
if (!serverApi.isSonarCloud()) {
return;
}

try {
var userId = serverApi.users().getCurrentUserId(cancelMonitor);
if (userId != null && !userId.trim().isEmpty()) {
storage.user().store(userId.trim());
}
} catch (Exception e) {
LOG.warn("Failed to synchronize user id from server: {}", e.getMessage());
}
}
}


Loading
Loading