import static org.sonar.db.user.UserDto.SCM_ACCOUNTS_SEPARATOR;
public class UserDao implements Dao {
-
+ private final static long WEEK_IN_MS = 7L * 24L * 3_600L * 1_000L;
private final System2 system2;
private final UuidFactory uuidFactory;
return dto;
}
+ public void updateSonarlintLastConnectionDate(DbSession session, String login) {
+ mapper(session).updateSonarlintLastConnectionDate(login, system2.now());
+ }
+
public void setRoot(DbSession session, String login, boolean root) {
mapper(session).setRoot(login, root, system2.now());
}
return mapper(dbSession).selectByExternalLoginAndIdentityProvider(externalLogin, externalIdentityProvider);
}
+ public long countSonarlintWeeklyUsers(DbSession dbSession) {
+ long threshold = system2.now() - WEEK_IN_MS;
+ return mapper(dbSession).countActiveSonarlintUsers(threshold);
+ }
+
public void scrollByUuids(DbSession dbSession, Collection<String> uuids, Consumer<UserDto> consumer) {
UserMapper mapper = mapper(dbSession);
@Nullable
private Long lastConnectionDate;
+ /**
+ * Date of the last time sonarlint connected to sonarqube WSs with this user's authentication.
+ * Can be null when user has never been authenticated, or has not been authenticated since the creation of the column in SonarQube 8.8.
+ */
+ @Nullable
+ private Long lastSonarlintConnectionDate;
+
private Long createdAt;
private Long updatedAt;
return this;
}
+ @CheckForNull
+ public Long getLastSonarlintConnectionDate() {
+ return lastSonarlintConnectionDate;
+ }
+
+ public UserDto setLastSonarlintConnectionDate(@Nullable Long lastSonarlintConnectionDate) {
+ this.lastSonarlintConnectionDate = lastSonarlintConnectionDate;
+ return this;
+ }
+
+
public Long getCreatedAt() {
return createdAt;
}
void scrollAll(ResultHandler<UserDto> handler);
+ void updateSonarlintLastConnectionDate(@Param("login") String login, @Param("now") long now);
+
/**
* Count actives users which are root and which login is not the specified one.
*/
void clearHomepages(@Param("homepageType") String type, @Param("homepageParameter") String value, @Param("now") long now);
void clearHomepage(@Param("login") String login, @Param("now") long now);
+
+ long countActiveSonarlintUsers(@Param("sinceDate") long sinceDate);
}
u.homepage_type as "homepageType",
u.homepage_parameter as "homepageParameter",
u.last_connection_date as "lastConnectionDate",
+ u.last_sonarlint_connection as "lastSonarlintConnectionDate",
u.created_at as "createdAt",
u.updated_at as "updatedAt"
</sql>
salt = null,
crypted_password = null,
last_connection_date = null,
+ last_sonarlint_connection = null,
updated_at = #{now, jdbcType=BIGINT}
</sql>
+ <update id="updateSonarlintLastConnectionDate" parameterType="map">
+ update users set
+ last_sonarlint_connection = #{now, jdbcType=BIGINT}
+ where
+ login = #{login, jdbcType=VARCHAR}
+ </update>
+
<update id="deactivateUser" parameterType="map">
update users set
<include refid="deactivateUserUpdatedFields"/>
hash_method,
is_root,
onboarded,
+ last_sonarlint_connection,
reset_password,
homepage_type,
homepage_parameter,
#{user.hashMethod,jdbcType=VARCHAR},
#{user.root,jdbcType=BOOLEAN},
#{user.onboarded,jdbcType=BOOLEAN},
+ #{user.lastSonarlintConnectionDate,jdbcType=BIGINT},
#{user.resetPassword,jdbcType=BOOLEAN},
#{user.homepageType,jdbcType=VARCHAR},
#{user.homepageParameter,jdbcType=VARCHAR},
homepage_type = #{user.homepageType, jdbcType=VARCHAR},
homepage_parameter = #{user.homepageParameter, jdbcType=VARCHAR},
last_connection_date = #{user.lastConnectionDate,jdbcType=BIGINT},
+ last_sonarlint_connection = #{user.lastSonarlintConnectionDate,jdbcType=BIGINT},
updated_at = #{user.updatedAt,jdbcType=BIGINT}
where
uuid = #{user.uuid, jdbcType=VARCHAR}
</update>
+ <select id="countActiveSonarlintUsers" parameterType="map" resultType="long">
+ select count(login) from users
+ where last_sonarlint_connection > #{sinceDate,jdbcType=BIGINT}
+ </select>
+
</mapper>
"LAST_CONNECTION_DATE" BIGINT,
"CREATED_AT" BIGINT,
"UPDATED_AT" BIGINT,
- "RESET_PASSWORD" BOOLEAN NOT NULL
+ "RESET_PASSWORD" BOOLEAN NOT NULL,
+ "LAST_SONARLINT_CONNECTION" BIGINT
);
ALTER TABLE "USERS" ADD CONSTRAINT "PK_USERS" PRIMARY KEY("UUID");
CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS"("LOGIN");
assertThat(untouchedUserReloaded.getHomepageParameter()).isEqualTo(untouchedUser.getHomepageParameter());
}
+ @Test
+ public void update_last_sonarlint_connection_date() {
+ UserDto user = db.users().insertUser();
+ assertThat(user.getLastSonarlintConnectionDate()).isNull();
+ underTest.updateSonarlintLastConnectionDate(db.getSession(), user.getLogin());
+ assertThat(underTest.selectByLogin(db.getSession(), user.getLogin()).getLastSonarlintConnectionDate()).isEqualTo(NOW);
+ }
+
+ @Test
+ public void count_sonarlint_weekly_users() {
+ UserDto user1 = db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 100_000));
+ UserDto user2 = db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW));
+ // these don't count
+ UserDto user3 = db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 1_000_000_000));
+ UserDto user4 = db.users().insertUser();
+
+ assertThat(underTest.countSonarlintWeeklyUsers(db.getSession())).isEqualTo(2);
+ }
+
@Test
public void clean_user_homepage() {
.extracting(UserDto::getUuid).containsExactlyInAnyOrder(user1.getUuid());
assertThat(underTest.selectByExternalIdsAndIdentityProvider(session,
asList(user1.getExternalId(), user2.getExternalId(), user3.getExternalId(), disableUser.getExternalId()), "github"))
- .extracting(UserDto::getUuid).containsExactlyInAnyOrder(user1.getUuid(), user2.getUuid(), disableUser.getUuid());
+ .extracting(UserDto::getUuid).containsExactlyInAnyOrder(user1.getUuid(), user2.getUuid(), disableUser.getUuid());
assertThat(underTest.selectByExternalIdsAndIdentityProvider(session, singletonList("unknown"), "github")).isEmpty();
assertThat(underTest.selectByExternalIdsAndIdentityProvider(session, singletonList(user1.getExternalId()), "unknown")).isEmpty();
}
import org.sonar.server.platform.db.migration.version.v85.DbVersion85;
import org.sonar.server.platform.db.migration.version.v86.DbVersion86;
import org.sonar.server.platform.db.migration.version.v87.DbVersion87;
+import org.sonar.server.platform.db.migration.version.v88.DbVersion88;
public class MigrationConfigurationModule extends Module {
@Override
DbVersion85.class,
DbVersion86.class,
DbVersion87.class,
+ DbVersion88.class,
// migration steps
MigrationStepRegistryImpl.class,
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.sonar.server.platform.db.migration.version.v88;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.def.BigIntegerColumnDef;
+import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class AddLastSonarlintConnectionToUsers extends DdlChange {
+ public AddLastSonarlintConnectionToUsers(Database db) {
+ super(db);
+ }
+
+ @Override
+ public void execute(Context context) throws SQLException {
+ context.execute(new AddColumnsBuilder(getDialect(), "users")
+ .addColumn(BigIntegerColumnDef.newBigIntegerColumnDefBuilder().setColumnName("last_sonarlint_connection").setIsNullable(true).build())
+ .build());
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.sonar.server.platform.db.migration.version.v88;
+
+import org.sonar.server.platform.db.migration.step.MigrationStepRegistry;
+import org.sonar.server.platform.db.migration.version.DbVersion;
+
+public class DbVersion88 implements DbVersion {
+
+ @Override
+ public void addSteps(MigrationStepRegistry registry) {
+ registry
+ .add(4300, "Add 'last_sonarlint_connection' to 'users", AddLastSonarlintConnectionToUsers.class)
+ ;
+ }
+}
private final DbVersion underTest = new DbVersion87();
@Test
- public void migrationNumber_starts_at_4100() {
+ public void migrationNumber_starts_at_4200() {
verifyMinimumMigrationNumber(underTest, 4200);
}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.sonar.server.platform.db.migration.version.v88;
+
+import java.sql.SQLException;
+import java.sql.Types;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+public class AddLastSonarlintConnectionToUsersTest {
+ private static final String TABLE_NAME = "users";
+ private static final String COLUMN_NAME = "last_sonarlint_connection";
+
+ @Rule
+ public CoreDbTester db = CoreDbTester.createForSchema(AddLastSonarlintConnectionToUsersTest.class, "schema.sql");
+ private final DdlChange underTest = new AddLastSonarlintConnectionToUsers(db.database());
+
+ @Test
+ public void add_column() throws SQLException {
+ db.assertColumnDoesNotExist(TABLE_NAME, COLUMN_NAME);
+ underTest.execute();
+ db.assertColumnDefinition(TABLE_NAME, COLUMN_NAME, Types.BIGINT, null, true);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.sonar.server.platform.db.migration.version.v88;
+
+import org.junit.Test;
+import org.sonar.server.platform.db.migration.version.DbVersion;
+
+import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMigrationNotEmpty;
+import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMinimumMigrationNumber;
+
+public class DbVersion88Test {
+
+ private final DbVersion underTest = new DbVersion88();
+
+ @Test
+ public void migrationNumber_starts_at_4300() {
+ verifyMinimumMigrationNumber(underTest, 4300);
+ }
+
+ @Test
+ public void verify_migration_count() {
+ verifyMigrationNotEmpty(underTest);
+ }
+
+}
--- /dev/null
+CREATE TABLE "USERS"(
+ "UUID" VARCHAR(255) NOT NULL,
+ "LOGIN" VARCHAR(255) NOT NULL,
+ "NAME" VARCHAR(200),
+ "EMAIL" VARCHAR(100),
+ "CRYPTED_PASSWORD" VARCHAR(100),
+ "SALT" VARCHAR(40),
+ "HASH_METHOD" VARCHAR(10),
+ "ACTIVE" BOOLEAN DEFAULT TRUE,
+ "SCM_ACCOUNTS" VARCHAR(4000),
+ "EXTERNAL_LOGIN" VARCHAR(255) NOT NULL,
+ "EXTERNAL_IDENTITY_PROVIDER" VARCHAR(100) NOT NULL,
+ "EXTERNAL_ID" VARCHAR(255) NOT NULL,
+ "IS_ROOT" BOOLEAN NOT NULL,
+ "USER_LOCAL" BOOLEAN,
+ "ONBOARDED" BOOLEAN NOT NULL,
+ "HOMEPAGE_TYPE" VARCHAR(40),
+ "HOMEPAGE_PARAMETER" VARCHAR(40),
+ "LAST_CONNECTION_DATE" BIGINT,
+ "CREATED_AT" BIGINT,
+ "UPDATED_AT" BIGINT,
+ "RESET_PASSWORD" BOOLEAN NOT NULL
+);
+ALTER TABLE "USERS" ADD CONSTRAINT "PK_USERS" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "USERS_LOGIN" ON "USERS"("LOGIN");
+CREATE INDEX "USERS_UPDATED_AT" ON "USERS"("UPDATED_AT");
+CREATE UNIQUE INDEX "UNIQ_EXTERNAL_ID" ON "USERS"("EXTERNAL_IDENTITY_PROVIDER", "EXTERNAL_ID");
+CREATE UNIQUE INDEX "UNIQ_EXTERNAL_LOGIN" ON "USERS"("EXTERNAL_IDENTITY_PROVIDER", "EXTERNAL_LOGIN");
private final Boolean hasUnanalyzedC;
private final Boolean hasUnanalyzedCpp;
private final List<String> customSecurityConfigs;
+ private final long sonarlintWeeklyUsers;
private TelemetryData(Builder builder) {
serverId = builder.serverId;
projectCount = builder.projectMeasuresStatistics.getProjectCount();
usingBranches = builder.usingBranches;
database = builder.database;
+ sonarlintWeeklyUsers = builder.sonarlintWeeklyUsers;
projectCountByLanguage = builder.projectMeasuresStatistics.getProjectCountByLanguage();
almIntegrationCountByAlm = builder.almIntegrationCountByAlm;
nclocByLanguage = builder.projectMeasuresStatistics.getNclocByLanguage();
return ncloc;
}
+ public long sonarlintWeeklyUsers() {
+ return sonarlintWeeklyUsers;
+ }
+
public long getUserCount() {
return userCount;
}
private String serverId;
private String version;
private long userCount;
+ private long sonarlintWeeklyUsers;
private Map<String, String> plugins;
private Database database;
private ProjectMeasuresStatistics projectMeasuresStatistics;
return this;
}
+ Builder setSonarlintWeeklyUsers(long sonarlintWeeklyUsers) {
+ this.sonarlintWeeklyUsers = sonarlintWeeklyUsers;
+ return this;
+ }
+
Builder setServerId(String serverId) {
this.serverId = serverId;
return this;
});
json.endArray();
+ json.prop("sonarlintWeeklyUsers", statistics.sonarlintWeeklyUsers());
if (statistics.getInstallationDate() != null) {
json.prop("installationDate", statistics.getInstallationDate());
}
.setNcloc(42L)
.setExternalAuthenticationProviders(asList("github", "gitlab"))
.setProjectCountByScm(Collections.emptyMap())
+ .setSonarlintWeeklyUsers(10)
.setDatabase(new TelemetryData.Database("H2", "11"))
.setUsingBranches(true);
assertJson(json).isSimilarTo("{ \"externalAuthProviders\": [ \"github\", \"gitlab\" ] }");
}
+ @Test
+ public void write_sonarlint_weekly_users() {
+ TelemetryData data = SOME_TELEMETRY_DATA.build();
+
+ String json = writeTelemetryData(data);
+
+ assertJson(json).isSimilarTo("{ \"sonarlintWeeklyUsers\": 10 }");
+ }
+
@Test
@UseDataProvider("allEditions")
public void writes_edition_if_non_null(EditionProvider.Edition edition) {
}
}
+ @Override
+ @CheckForNull
+ public Long getLastSonarlintConnectionDate() {
+ return null;
+ }
+
@Override
public final boolean hasPermission(GlobalPermission permission) {
return isRoot() || hasPermissionImpl(permission);
private Set<GlobalPermission> permissions;
private Map<String, Set<String>> permissionsByProjectUuid;
- ServerUserSession(DbClient dbClient, @Nullable UserDto userDto) {
+ public ServerUserSession(DbClient dbClient, @Nullable UserDto userDto) {
this.dbClient = dbClient;
this.userDto = userDto;
}
}
}
+ @Override
+ @CheckForNull
+ public Long getLastSonarlintConnectionDate() {
+ return userDto == null ? null : userDto.getLastSonarlintConnectionDate();
+ }
+
@Override
@CheckForNull
public String getLogin() {
return DELEGATE.get() != null;
}
+ @Override
+ @CheckForNull
+ public Long getLastSonarlintConnectionDate() {
+ return get().getLastSonarlintConnectionDate();
+ }
+
@Override
@CheckForNull
public String getLogin() {
@CheckForNull
String getName();
+ @CheckForNull
+ Long getLastSonarlintConnectionDate();
+
/**
* The groups that the logged-in user is member of. An empty
* collection is returned if {@link #isLoggedIn()} is {@code false}.
assertThat(newUserSession(user).getGroups()).extracting(GroupDto::getUuid).containsOnly(group1.getUuid(), group2.getUuid());
}
+ @Test
+ public void getLastSonarlintConnectionDate() {
+ UserDto user = db.users().insertUser(p -> p.setLastSonarlintConnectionDate(1000L));
+ assertThat(newUserSession(user).getLastSonarlintConnectionDate()).isEqualTo(1000L);
+ }
+
@Test
public void getGroups_keeps_groups_in_cache() {
UserDto user = db.users().insertUser();
MockUserSession expected = new MockUserSession("karadoc")
.setUuid("karadoc-uuid")
.setResetPassword(true)
+ .setLastSonarlintConnectionDate(1000L)
.setGroups(group);
threadLocalUserSession.set(expected);
UserSession session = threadLocalUserSession.get();
assertThat(session).isSameAs(expected);
+ assertThat(threadLocalUserSession.getLastSonarlintConnectionDate()).isEqualTo(1000L);
assertThat(threadLocalUserSession.getLogin()).isEqualTo("karadoc");
assertThat(threadLocalUserSession.getUuid()).isEqualTo("karadoc-uuid");
assertThat(threadLocalUserSession.isLoggedIn()).isTrue();
import java.util.Collection;
import java.util.List;
import java.util.Optional;
+import javax.annotation.CheckForNull;
+import org.jetbrains.annotations.Nullable;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.user.AbstractUserSession;
private List<GroupDto> groups = new ArrayList<>();
private UserSession.IdentityProvider identityProvider;
private UserSession.ExternalIdentity externalIdentity;
+ private Long lastSonarlintConnectionDate;
public MockUserSession(String login) {
super(MockUserSession.class);
this.externalIdentity = identity.getExternalIdentity();
}
+ @CheckForNull
+ @Override
+ public Long getLastSonarlintConnectionDate() {
+ return lastSonarlintConnectionDate;
+ }
+
+ public MockUserSession setLastSonarlintConnectionDate(@Nullable Long lastSonarlintConnectionDate) {
+ this.lastSonarlintConnectionDate = lastSonarlintConnectionDate;
+ return this;
+ }
+
@Override
public boolean isLoggedIn() {
return true;
return currentUserSession.getName();
}
+ @Override
+ @CheckForNull
+ public Long getLastSonarlintConnectionDate() {
+ return currentUserSession.getLastSonarlintConnectionDate();
+ }
+
@Override
public Collection<GroupDto> getGroups() {
return currentUserSession.getGroups();
.stream()
.collect(Collectors.toMap(ProjectCountPerAnalysisPropertyValue::getPropertyValue, ProjectCountPerAnalysisPropertyValue::getCount));
data.setProjectCountByScm(projectCountPerScmDetected);
+ data.setSonarlintWeeklyUsers(dbClient.userDao().countSonarlintWeeklyUsers(dbSession));
}
setSecurityCustomConfigIfPresent(data);
Optional<String> installationVersionProperty = internalProperties.read(InternalProperties.INSTALLATION_VERSION);
data.setInstallationVersion(installationVersionProperty.orElse(null));
data.setInDocker(dockerSupport.isRunningInDocker());
-
return data.build();
}
+ "\"Application Nodes\":[{\"Name\":\"appNodes\",\"\":{\"name\":\"appNodes\"}}],"
+ "\"Search Nodes\":[{\"Name\":\"searchNodes\",\"\":{\"name\":\"searchNodes\"}}],"
+ "\"Statistics\":{\"id\":\"\",\"version\":\"\",\"database\":{\"name\":\"\",\"version\":\"\"},\"plugins\":[],"
- + "\"userCount\":0,\"projectCount\":0,\"usingBranches\":false,\"ncloc\":0,\"projectCountByLanguage\":[]," +
- "\"nclocByLanguage\":[],\"almIntegrationCount\":[],\"externalAuthProviders\":[],\"projectCountByScm\":[],\"installationDate\":0,\"installationVersion\":\"\",\"docker\":false}}");
+ + "\"userCount\":0,\"projectCount\":0,\"usingBranches\":false,\"ncloc\":0,\"projectCountByLanguage\":[],"
+ + "\"nclocByLanguage\":[],\"almIntegrationCount\":[],\"externalAuthProviders\":[],\"projectCountByScm\":[],\"sonarlintWeeklyUsers\":0,\"installationDate\":0,"
+ + "\"installationVersion\":\"\",\"docker\":false}}");
}
private static NodeInfo createNodeInfo(String name) {
// response does not contain empty "Section Three"
assertThat(writer).hasToString("{\"Health\":\"GREEN\",\"Health Causes\":[],\"Section One\":{\"foo\":\"bar\"},\"Section Two\":{\"one\":1,\"two\":2}," +
"\"Statistics\":{\"id\":\"\",\"version\":\"\",\"database\":{\"name\":\"\",\"version\":\"\"},\"plugins\":[],\"userCount\":0,\"projectCount\":0,\"usingBranches\":false," +
- "\"ncloc\":0,\"projectCountByLanguage\":[],\"nclocByLanguage\":[],\"almIntegrationCount\":[],\"externalAuthProviders\":[],\"projectCountByScm\":[],\"installationDate\":0,\"installationVersion\":\"\",\"docker\":false}}");
+ "\"ncloc\":0,\"projectCountByLanguage\":[],\"nclocByLanguage\":[],\"almIntegrationCount\":[],\"externalAuthProviders\":[],\"projectCountByScm\":[],"
+ + "\"sonarlintWeeklyUsers\":0,\"installationDate\":0,\"installationVersion\":\"\",\"docker\":false}}");
}
private void logInAsSystemAdministrator() {
import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_C_KEY;
public class TelemetryDataLoaderImplTest {
+ private final static Long NOW = 100_000_000L;
+ private final TestSystem2 system2 = new TestSystem2().setNow(NOW);
+
@Rule
- public DbTester db = DbTester.create();
+ public DbTester db = DbTester.create(system2);
@Rule
public EsTester es = EsTester.create();
private final FakeServer server = new FakeServer();
private final PluginRepository pluginRepository = mock(PluginRepository.class);
private final Configuration configuration = mock(Configuration.class);
- private final TestSystem2 system2 = new TestSystem2().setNow(System.currentTimeMillis());
private final PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
private final DockerSupport dockerSupport = mock(DockerSupport.class);
private final InternalProperties internalProperties = spy(new MapInternalProperties());
assertThat(data.getLicenseType()).isEmpty();
}
+ @Test
+ public void data_contains_weekly_count_sonarlint_users() {
+ db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 100_000L));
+ db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW));
+ // these don't count
+ db.users().insertUser(c -> c.setLastSonarlintConnectionDate(NOW - 1_000_000_000L));
+ db.users().insertUser();
+
+ TelemetryData data = communityUnderTest.load();
+ assertThat(data.sonarlintWeeklyUsers()).isEqualTo(2L);
+ }
+
@Test
public void data_has_license_type_on_commercial_edition_if_no_license() {
String licenseType = randomAlphabetic(12);
import org.sonar.server.platform.PersistentSettings;
import org.sonar.server.platform.SystemInfoWriterModule;
import org.sonar.server.platform.WebCoreExtensionsInstaller;
+import org.sonar.server.platform.web.SonarLintConnectionFilter;
import org.sonar.server.platform.web.WebServiceFilter;
import org.sonar.server.platform.web.WebServiceReroutingFilter;
import org.sonar.server.platform.web.requestid.HttpRequestIdModule;
// web services
WebServiceEngine.class,
WebServicesWsModule.class,
+ SonarLintConnectionFilter.class,
WebServiceFilter.class,
WebServiceReroutingFilter.class,
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.sonar.server.platform.web;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Optional;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import org.sonar.api.utils.System2;
+import org.sonar.api.web.ServletFilter;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.user.UserSession;
+import org.sonar.server.ws.ServletRequest;
+
+public class SonarLintConnectionFilter extends ServletFilter {
+ private static final UrlPattern URL_PATTERN = UrlPattern.builder()
+ .includes("/api/*")
+ .build();
+ private final DbClient dbClient;
+ private final UserSession userSession;
+ private final System2 system2;
+
+ public SonarLintConnectionFilter(DbClient dbClient, UserSession userSession, System2 system2) {
+ this.dbClient = dbClient;
+ this.userSession = userSession;
+ this.system2 = system2;
+ }
+
+ @Override
+ public UrlPattern doGetPattern() {
+ return URL_PATTERN;
+ }
+
+ @Override
+ public void doFilter(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
+ HttpServletRequest request = (HttpServletRequest) servletRequest;
+ ServletRequest wsRequest = new ServletRequest(request);
+
+ Optional<String> agent = wsRequest.header("User-Agent");
+ if (agent.isPresent() && agent.get().toLowerCase(Locale.ENGLISH).contains("sonarlint")) {
+ update();
+ }
+ chain.doFilter(servletRequest, servletResponse);
+ }
+
+ @Override
+ public void init(FilterConfig filterConfig) {
+ // Nothing to do
+ }
+
+ @Override
+ public void destroy() {
+ // Nothing to do
+ }
+
+ public void update() {
+ if (shouldUpdate()) {
+ try (DbSession session = dbClient.openSession(false)) {
+ dbClient.userDao().updateSonarlintLastConnectionDate(session, userSession.getLogin());
+ session.commit();
+ }
+ }
+ }
+
+ private boolean shouldUpdate() {
+ if (!userSession.isLoggedIn()) {
+ return false;
+ }
+ long now = system2.now();
+ Long lastUpdate = userSession.getLastSonarlintConnectionDate();
+ return (lastUpdate == null || lastUpdate < now - 3_600_000L);
+ }
+}
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
-import org.sonar.api.SonarRuntime;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.web.ServletFilter;
import org.sonar.core.util.stream.MoreCollectors;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 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.sonar.server.platform.web;
+
+import java.io.IOException;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.impl.utils.TestSystem2;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.UserDto;
+import org.sonar.server.user.ServerUserSession;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class SonarLintConnectionFilterTest {
+ private static final String LOGIN = "user1";
+ private final TestSystem2 system2 = new TestSystem2();
+
+ @Rule
+ public DbTester dbTester = DbTester.create(system2);
+
+ @Test
+ public void update() throws IOException, ServletException {
+ system2.setNow(10_000_000L);
+ addUser(LOGIN, 1_000_000L);
+
+ runFilter(LOGIN, "SonarLint for IntelliJ");
+ assertThat(getLastUpdate(LOGIN)).isEqualTo(10_000_000L);
+ }
+
+ @Test
+ public void update_first_time() throws IOException, ServletException {
+ system2.setNow(10_000_000L);
+ addUser(LOGIN, null);
+
+ runFilter(LOGIN, "SonarLint for IntelliJ");
+ assertThat(getLastUpdate(LOGIN)).isEqualTo(10_000_000L);
+ }
+
+ @Test
+ public void only_applies_to_api() {
+ SonarLintConnectionFilter underTest = new SonarLintConnectionFilter(dbTester.getDbClient(), mock(ServerUserSession.class), system2);
+ assertThat(underTest.doGetPattern().matches("/api/test")).isTrue();
+ assertThat(underTest.doGetPattern().matches("/test")).isFalse();
+
+ }
+
+ @Test
+ public void do_nothing_if_no_sonarlint_agent() throws IOException, ServletException {
+ system2.setNow(10_000L);
+ addUser(LOGIN, 1_000L);
+
+ runFilter(LOGIN, "unknown");
+ runFilter(LOGIN, null);
+ assertThat(getLastUpdate(LOGIN)).isEqualTo(1_000L);
+ }
+
+ @Test
+ public void do_nothing_if_not_logged_in() throws IOException, ServletException {
+ system2.setNow(10_000_000L);
+ addUser("invalid", 1_000_000L);
+
+ runFilter(LOGIN, "SonarLint for IntelliJ");
+ assertThat(getLastUpdate("invalid")).isEqualTo(1_000_000L);
+ }
+
+ @Test
+ public void only_update_if_not_updated_within_1h() throws IOException, ServletException {
+ system2.setNow(2_000_000L);
+ addUser(LOGIN, 1_000_000L);
+
+ runFilter(LOGIN, "SonarLint for IntelliJ");
+ assertThat(getLastUpdate(LOGIN)).isEqualTo(1_000_000L);
+ }
+
+ private void addUser(String login, @Nullable Long lastUpdate) {
+ dbTester.users().insertUser(u -> u.setLogin(login).setLastSonarlintConnectionDate(lastUpdate));
+ }
+
+ @CheckForNull
+ private Long getLastUpdate(String login) {
+ return dbTester.getDbClient().userDao().selectByLogin(dbTester.getSession(), login).getLastSonarlintConnectionDate();
+ }
+
+ private void runFilter(String loggedInUser, @Nullable String agent) throws IOException, ServletException {
+ UserDto user = dbTester.getDbClient().userDao().selectByLogin(dbTester.getSession(), loggedInUser);
+ ServerUserSession session = new ServerUserSession(dbTester.getDbClient(), user);
+ SonarLintConnectionFilter underTest = new SonarLintConnectionFilter(dbTester.getDbClient(), session, system2);
+ HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getHeader("User-Agent")).thenReturn(agent);
+ FilterChain chain = mock(FilterChain.class);
+ underTest.doFilter(request, mock(ServletResponse.class), chain);
+ verify(chain).doFilter(any(), any());
+ }
+}