]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14518 Telemetry - include number of sonarlint users
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Thu, 25 Feb 2021 21:21:10 +0000 (15:21 -0600)
committersonartech <sonartech@sonarsource.com>
Thu, 4 Mar 2021 20:12:49 +0000 (20:12 +0000)
32 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
server/sonar-db-dao/src/schema/schema-sq.ddl
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDaoTest.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v88/AddLastSonarlintConnectionToUsers.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v88/DbVersion88.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v87/DbVersion87Test.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v88/AddLastSonarlintConnectionToUsersTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v88/DbVersion88Test.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v88/AddLastSonarlintConnectionToUsersTest/schema.sql [new file with mode: 0644]
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryData.java
server/sonar-server-common/src/main/java/org/sonar/server/telemetry/TelemetryDataJsonWriter.java
server/sonar-server-common/src/test/java/org/sonar/server/telemetry/TelemetryDataJsonWriterTest.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/AbstractUserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ServerUserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java
server/sonar-webserver-auth/src/main/java/org/sonar/server/user/UserSession.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ServerUserSessionTest.java
server/sonar-webserver-auth/src/test/java/org/sonar/server/user/ThreadLocalUserSessionTest.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/MockUserSession.java
server/sonar-webserver-auth/src/testFixtures/java/org/sonar/server/tester/UserSessionRule.java
server/sonar-webserver-core/src/main/java/org/sonar/server/telemetry/TelemetryDataLoaderImpl.java
server/sonar-webserver-core/src/test/java/org/sonar/server/platform/ClusterSystemInfoWriterTest.java
server/sonar-webserver-core/src/test/java/org/sonar/server/platform/StandaloneSystemInfoWriterTest.java
server/sonar-webserver-core/src/test/java/org/sonar/server/telemetry/TelemetryDataLoaderImplTest.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SonarLintConnectionFilter.java [new file with mode: 0644]
server/sonar-webserver/src/main/java/org/sonar/server/platform/web/WebServiceFilter.java
server/sonar-webserver/src/test/java/org/sonar/server/platform/web/SonarLintConnectionFilterTest.java [new file with mode: 0644]

index b498d86c0ddb6b990f908c6a5872da386a7adf7d..8fe12dcae2a149f0272294aa6c13b3025cf777f1 100644 (file)
@@ -43,7 +43,7 @@ import static org.sonar.db.DatabaseUtils.executeLargeInputsWithoutOutput;
 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;
 
@@ -112,6 +112,10 @@ public class UserDao implements Dao {
     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());
   }
@@ -171,6 +175,11 @@ public class UserDao implements Dao {
     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);
 
index 251aaa1373cb87a45c317ae4cb08f1548323a5ff..5ed11212836da8ef425592dada44e0656b1861ce 100644 (file)
@@ -63,6 +63,13 @@ public class UserDto {
   @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;
 
@@ -281,6 +288,17 @@ public class UserDto {
     return this;
   }
 
+  @CheckForNull
+  public Long getLastSonarlintConnectionDate() {
+    return lastSonarlintConnectionDate;
+  }
+
+  public UserDto setLastSonarlintConnectionDate(@Nullable Long lastSonarlintConnectionDate) {
+    this.lastSonarlintConnectionDate = lastSonarlintConnectionDate;
+    return this;
+  }
+
+
   public Long getCreatedAt() {
     return createdAt;
   }
index 930ce1d9464e045bb6eeed3171799d632084336c..0f76f7216672091f975c6953e96accb348775879 100644 (file)
@@ -66,6 +66,8 @@ public interface UserMapper {
 
   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.
    */
@@ -82,4 +84,6 @@ public interface UserMapper {
   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);
 }
index 323ef1d27bb08ab34b2559d15538d8e9712a8096..6f6a3daa2a0a0f375147da55255fd2f3e33ef3ec 100644 (file)
@@ -23,6 +23,7 @@
         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>
index 35c44d970c069a6405bede4b0c2fcd0114428ba4..55bc6dbb64ef2cc5e11f8183d731667c4812d24a 100644 (file)
@@ -914,7 +914,8 @@ CREATE TABLE "USERS"(
     "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");
index d69d1ce652992f94d011765ba5b9f37b75368ff5..8922f20e22dcff2b2c41b166304a3bd25722d557 100644 (file)
@@ -510,6 +510,25 @@ public class UserDaoTest {
     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() {
 
@@ -628,7 +647,7 @@ public class UserDaoTest {
       .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();
   }
index bb1008a495097c283088967e2833f72eb9950068..1c4356cc5d0587cbff76a3c0743ad5761e1e6da6 100644 (file)
@@ -35,6 +35,7 @@ import org.sonar.server.platform.db.migration.version.v84.DbVersion84;
 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
@@ -50,6 +51,7 @@ public class MigrationConfigurationModule extends Module {
       DbVersion85.class,
       DbVersion86.class,
       DbVersion87.class,
+      DbVersion88.class,
 
       // migration steps
       MigrationStepRegistryImpl.class,
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v88/AddLastSonarlintConnectionToUsers.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v88/AddLastSonarlintConnectionToUsers.java
new file mode 100644 (file)
index 0000000..773117d
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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());
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v88/DbVersion88.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v88/DbVersion88.java
new file mode 100644 (file)
index 0000000..2c19ad0
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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)
+    ;
+  }
+}
index 291f331225a901605e2da40af13ec11040a6e67f..d495c37873c84226c2e64628cac56403917812c8 100644 (file)
@@ -30,7 +30,7 @@ public class DbVersion87Test {
   private final DbVersion underTest = new DbVersion87();
 
   @Test
-  public void migrationNumber_starts_at_4100() {
+  public void migrationNumber_starts_at_4200() {
     verifyMinimumMigrationNumber(underTest, 4200);
   }
 
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v88/AddLastSonarlintConnectionToUsersTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v88/AddLastSonarlintConnectionToUsersTest.java
new file mode 100644 (file)
index 0000000..2583cd7
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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);
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v88/DbVersion88Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v88/DbVersion88Test.java
new file mode 100644 (file)
index 0000000..3dbf9b6
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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);
+  }
+
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v88/AddLastSonarlintConnectionToUsersTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v88/AddLastSonarlintConnectionToUsersTest/schema.sql
new file mode 100644 (file)
index 0000000..92b679b
--- /dev/null
@@ -0,0 +1,28 @@
+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");
index b625d1aead5495bbd1bb8b79dac7ca6cd5f87633..a6d51d11204a556ab50757dd8ff51e1923f744b0 100644 (file)
@@ -52,6 +52,7 @@ public class TelemetryData {
   private final Boolean hasUnanalyzedC;
   private final Boolean hasUnanalyzedCpp;
   private final List<String> customSecurityConfigs;
+  private final long sonarlintWeeklyUsers;
 
   private TelemetryData(Builder builder) {
     serverId = builder.serverId;
@@ -62,6 +63,7 @@ public class TelemetryData {
     projectCount = builder.projectMeasuresStatistics.getProjectCount();
     usingBranches = builder.usingBranches;
     database = builder.database;
+    sonarlintWeeklyUsers = builder.sonarlintWeeklyUsers;
     projectCountByLanguage = builder.projectMeasuresStatistics.getProjectCountByLanguage();
     almIntegrationCountByAlm = builder.almIntegrationCountByAlm;
     nclocByLanguage = builder.projectMeasuresStatistics.getNclocByLanguage();
@@ -93,6 +95,10 @@ public class TelemetryData {
     return ncloc;
   }
 
+  public long sonarlintWeeklyUsers() {
+    return sonarlintWeeklyUsers;
+  }
+
   public long getUserCount() {
     return userCount;
   }
@@ -169,6 +175,7 @@ public class TelemetryData {
     private String serverId;
     private String version;
     private long userCount;
+    private long sonarlintWeeklyUsers;
     private Map<String, String> plugins;
     private Database database;
     private ProjectMeasuresStatistics projectMeasuresStatistics;
@@ -201,6 +208,11 @@ public class TelemetryData {
       return this;
     }
 
+    Builder setSonarlintWeeklyUsers(long sonarlintWeeklyUsers) {
+      this.sonarlintWeeklyUsers = sonarlintWeeklyUsers;
+      return this;
+    }
+
     Builder setServerId(String serverId) {
       this.serverId = serverId;
       return this;
index e675e1dc101c47a29225eeac60552f9283c00319..f427f0c5e28840c465f1ca2082bc8e001a68d4cf 100644 (file)
@@ -105,6 +105,7 @@ public class TelemetryDataJsonWriter {
     });
     json.endArray();
 
+    json.prop("sonarlintWeeklyUsers", statistics.sonarlintWeeklyUsers());
     if (statistics.getInstallationDate() != null) {
       json.prop("installationDate", statistics.getInstallationDate());
     }
index ca3e14f774cef2e8b4127c8c16170df553877f7b..2b0c918d9c5b3010e965276c29b1f3edb0336bbf 100644 (file)
@@ -59,6 +59,7 @@ public class TelemetryDataJsonWriterTest {
     .setNcloc(42L)
     .setExternalAuthenticationProviders(asList("github", "gitlab"))
     .setProjectCountByScm(Collections.emptyMap())
+    .setSonarlintWeeklyUsers(10)
     .setDatabase(new TelemetryData.Database("H2", "11"))
     .setUsingBranches(true);
 
@@ -96,6 +97,15 @@ public class TelemetryDataJsonWriterTest {
     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) {
index c0481b08875de1a95f8f49b8e1bbe05b1ed5bcff..9bf08d2ec8eeffcd33cc3fabb83180718c31aa2d 100644 (file)
@@ -75,6 +75,12 @@ public abstract class AbstractUserSession implements UserSession {
     }
   }
 
+  @Override
+  @CheckForNull
+  public Long getLastSonarlintConnectionDate() {
+    return null;
+  }
+
   @Override
   public final boolean hasPermission(GlobalPermission permission) {
     return isRoot() || hasPermissionImpl(permission);
index cf4091355386768682e3e90035850d7eb7ce5e72..c0909a7c68650093279c934c033a88117f44be41 100644 (file)
@@ -55,7 +55,7 @@ public class ServerUserSession extends AbstractUserSession {
   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;
   }
@@ -69,6 +69,12 @@ public class ServerUserSession extends AbstractUserSession {
     }
   }
 
+  @Override
+  @CheckForNull
+  public Long getLastSonarlintConnectionDate() {
+    return userDto == null ? null : userDto.getLastSonarlintConnectionDate();
+  }
+
   @Override
   @CheckForNull
   public String getLogin() {
index 8ca25ad35a3a3b0b67e42e9218c4cc4c9ef0ad80..d9b06b04c3f34f506fc9f6edf975a83e1ff324a7 100644 (file)
@@ -56,6 +56,12 @@ public class ThreadLocalUserSession implements UserSession {
     return DELEGATE.get() != null;
   }
 
+  @Override
+  @CheckForNull
+  public Long getLastSonarlintConnectionDate() {
+    return get().getLastSonarlintConnectionDate();
+  }
+
   @Override
   @CheckForNull
   public String getLogin() {
index 285c1f21f97eacf07e4882b1038dc0db99d2a3fb..4bd33d3c0a5e012263f503fa992366355f3bcaa2 100644 (file)
@@ -56,6 +56,9 @@ public interface UserSession {
   @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}.
index 827b94189aa46fc44af92435d1d7a505273a25c1..0d21463a0d5696a963b0958096e3d58534f3ed64 100644 (file)
@@ -99,6 +99,12 @@ public class ServerUserSessionTest {
     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();
index 85fb7fbb27c9284cc4268776145fc82e8039a861..1183f5b00545aee3f2599841c30ace50993e0d6f 100644 (file)
@@ -53,11 +53,13 @@ public class ThreadLocalUserSessionTest {
     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();
index 58d773094782a2e0500109cfc899c3072c7162e9..413ba96e6795f72b889441d05743cf1f65c14709 100644 (file)
@@ -23,6 +23,8 @@ import java.util.ArrayList;
 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;
@@ -41,6 +43,7 @@ public class MockUserSession extends AbstractMockUserSession<MockUserSession> {
   private List<GroupDto> groups = new ArrayList<>();
   private UserSession.IdentityProvider identityProvider;
   private UserSession.ExternalIdentity externalIdentity;
+  private Long lastSonarlintConnectionDate;
 
   public MockUserSession(String login) {
     super(MockUserSession.class);
@@ -62,6 +65,17 @@ public class MockUserSession extends AbstractMockUserSession<MockUserSession> {
     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;
index 6301f90f2fdbefca1baaee7b22b4ff46037bcf14..d85fa44e2c2541558d017fbd6b55c3f426954501 100644 (file)
@@ -278,6 +278,12 @@ public class UserSessionRule implements TestRule, UserSession {
     return currentUserSession.getName();
   }
 
+  @Override
+  @CheckForNull
+  public Long getLastSonarlintConnectionDate() {
+    return currentUserSession.getLastSonarlintConnectionDate();
+  }
+
   @Override
   public Collection<GroupDto> getGroups() {
     return currentUserSession.getGroups();
index c944b32e8577c2903157d472d933dff679fb4efe..16354f7b35907577723beb43ac27f951c7bc11f8 100644 (file)
@@ -144,6 +144,7 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
         .stream()
         .collect(Collectors.toMap(ProjectCountPerAnalysisPropertyValue::getPropertyValue, ProjectCountPerAnalysisPropertyValue::getCount));
       data.setProjectCountByScm(projectCountPerScmDetected);
+      data.setSonarlintWeeklyUsers(dbClient.userDao().countSonarlintWeeklyUsers(dbSession));
     }
 
     setSecurityCustomConfigIfPresent(data);
@@ -153,7 +154,6 @@ public class TelemetryDataLoaderImpl implements TelemetryDataLoader {
     Optional<String> installationVersionProperty = internalProperties.read(InternalProperties.INSTALLATION_VERSION);
     data.setInstallationVersion(installationVersionProperty.orElse(null));
     data.setInDocker(dockerSupport.isRunningInDocker());
-
     return data.build();
   }
 
index 560e4461f19cdb7489861e1c70c8e831e1a6e1b3..e22bbe0cd20ce99be30bf6b41894618ee8f8442c 100644 (file)
@@ -73,8 +73,9 @@ public class ClusterSystemInfoWriterTest {
       + "\"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) {
index c22757e9e81f58d12942bb8b16a56d5111138f24..d9e68091ae125a22ebaa36ef8f0ed6b5c709eb45 100644 (file)
@@ -80,7 +80,8 @@ public class StandaloneSystemInfoWriterTest {
     // 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() {
index dd78c60e233d18943b8eb4abceb4f4e1c77dc008..4ec49748d9b5fe28acae43b0417c7e6011236ec3 100644 (file)
@@ -66,15 +66,17 @@ import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_CPP_K
 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());
@@ -201,6 +203,18 @@ public class TelemetryDataLoaderImplTest {
     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);
index 528fd1c473c607bf464f031d502437b592eb9d2a..c3d821a83537b5d4e0ad9068b98732cf88b8b47c 100644 (file)
@@ -133,6 +133,7 @@ import org.sonar.server.platform.ClusterVerification;
 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;
@@ -338,6 +339,7 @@ public class PlatformLevel4 extends PlatformLevel {
       // web services
       WebServiceEngine.class,
       WebServicesWsModule.class,
+      SonarLintConnectionFilter.class,
       WebServiceFilter.class,
       WebServiceReroutingFilter.class,
 
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SonarLintConnectionFilter.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/web/SonarLintConnectionFilter.java
new file mode 100644 (file)
index 0000000..2b11c6a
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * 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);
+  }
+}
index 91225b72daa51018ec05a4ec15db2e81da7c818c..794c545a8072703340f5cc7ee65cb579a44c27d3 100644 (file)
@@ -26,7 +26,6 @@ import javax.servlet.FilterChain;
 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;
diff --git a/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/SonarLintConnectionFilterTest.java b/server/sonar-webserver/src/test/java/org/sonar/server/platform/web/SonarLintConnectionFilterTest.java
new file mode 100644 (file)
index 0000000..53285db
--- /dev/null
@@ -0,0 +1,122 @@
+/*
+ * 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());
+  }
+}