You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ignite.apache.org by GitBox <gi...@apache.org> on 2018/11/10 13:06:09 UTC

[GitHub] asfgit closed pull request #40: IGNITE-9939 [Tc Bot] Add visas caсhing and monitoring

asfgit closed pull request #40: IGNITE-9939 [Tc Bot] Add visas caсhing and monitoring
URL: https://github.com/apache/ignite-teamcity-bot/pull/40
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITcHelper.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITcHelper.java
index 25a5bd0f..7531e1c1 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITcHelper.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/ITcHelper.java
@@ -21,9 +21,12 @@
 import java.util.List;
 import org.apache.ignite.ci.issue.IssueDetector;
 import org.apache.ignite.ci.issue.IssuesStorage;
+import org.apache.ignite.ci.web.model.CompactVisa;
 import org.apache.ignite.ci.teamcity.restcached.ITcServerProvider;
 import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.user.UserAndSessionsStorage;
+import org.apache.ignite.ci.web.model.Visa;
+import org.apache.ignite.ci.web.model.hist.VisasHistoryStorage;
 
 /**
  * Teamcity Bot main interface. This inteface became too huge.
@@ -67,7 +70,7 @@
      * @param buildTypeId Suite name.
      * @param branchForTc Branch for TeamCity.
      * @param ticket JIRA ticket full name.
-     * @return {@code True} if JIRA was notified.
+     * @return {@code Visa} which contains info about JIRA notification.
      */
-    String notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc, String ticket);
+    Visa notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc, String ticket);
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java
index b40ba693..3c56b4e2 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/TcHelper.java
@@ -17,12 +17,15 @@
 
 package org.apache.ignite.ci;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.ignite.ci.tcbot.chain.PrChainsProcessor;
 import org.apache.ignite.ci.conf.BranchesTracked;
 import org.apache.ignite.ci.issue.IssueDetector;
 import org.apache.ignite.ci.issue.IssuesStorage;
 import org.apache.ignite.ci.jira.IJiraIntegration;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
+import org.apache.ignite.ci.web.model.JiraCommentResponse;
+import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.tcmodel.result.problems.ProblemOccurrence;
 import org.apache.ignite.ci.teamcity.restcached.ITcServerProvider;
 import org.apache.ignite.ci.user.ICredentialsProv;
@@ -68,7 +71,11 @@
 
     @Inject private PrChainsProcessor prChainsProcessor;
 
+    /** */
+    private final ObjectMapper objectMapper;
+
     public TcHelper() {
+        objectMapper = new ObjectMapper();
     }
 
     /** {@inheritDoc} */
@@ -132,7 +139,7 @@ private BranchesTracked getTrackedBranches() {
     }
 
     /** {@inheritDoc} */
-    @Override public String notifyJira(
+    @Override public Visa notifyJira(
         String srvId,
         ICredentialsProv prov,
         String buildTypeId,
@@ -144,15 +151,23 @@ private BranchesTracked getTrackedBranches() {
         List<BuildRef> builds = teamcity.getFinishedBuildsIncludeSnDepFailed(buildTypeId, branchForTc);
 
         if (builds.isEmpty())
-            return "JIRA wasn't commented - no finished builds to analyze.";
+            return new Visa("JIRA wasn't commented - no finished builds to analyze.");
 
         BuildRef build = builds.get(builds.size() - 1);
-        String comment;
+
+        int blockers;
+
+        JiraCommentResponse res;
 
         try {
-            comment = generateJiraComment(buildTypeId, build.branchName, srvId, prov, build.webUrl);
+            List<SuiteCurrentStatus> suitesStatuses =  getSuitesStatuses(buildTypeId, build.branchName, srvId, prov);
 
-            teamcity.sendJiraComment(ticket, comment);
+            String comment = generateJiraComment(suitesStatuses, build.webUrl);
+
+            blockers = suitesStatuses.stream().mapToInt(suite ->
+                suite.testFailures.size()).sum();
+
+            res = objectMapper.readValue(teamcity.sendJiraComment(ticket, comment), JiraCommentResponse.class);
         }
         catch (Exception e) {
             String errMsg = "Exception happened during commenting JIRA ticket " +
@@ -160,10 +175,10 @@ private BranchesTracked getTrackedBranches() {
 
             logger.error(errMsg);
 
-            return "JIRA wasn't commented - " + errMsg;
+            return new Visa("JIRA wasn't commented - " + errMsg);
         }
 
-        return JIRA_COMMENTED;
+        return new Visa(JIRA_COMMENTED, res, blockers);
     }
 
     /**
@@ -171,17 +186,14 @@ private BranchesTracked getTrackedBranches() {
      * @param branchForTc Branch for TeamCity.
      * @param srvId Server id.
      * @param prov Credentials.
-     * @param webUrl Build URL.
-     * @return Comment, which should be sent to the JIRA ticket.
+     * @return List of suites with possible blockers.
      */
-    private String generateJiraComment(
-        String buildTypeId,
+    public List<SuiteCurrentStatus> getSuitesStatuses(String buildTypeId,
         String branchForTc,
         String srvId,
-        ICredentialsProv prov,
-        String webUrl
-    ) {
-        StringBuilder res = new StringBuilder();
+        ICredentialsProv prov) {
+        List<SuiteCurrentStatus> res = new ArrayList<>();
+
         TestFailuresSummary summary = prChainsProcessor.getTestFailuresSummary(
             prov, srvId, buildTypeId, branchForTc,
             FullQueryParams.LATEST, null, null, false);
@@ -193,54 +205,61 @@ private String generateJiraComment(
 
                 Map<String, List<SuiteCurrentStatus>> fails = findFailures(server);
 
-                for (List<SuiteCurrentStatus> suites : fails.values()) {
-                    for (SuiteCurrentStatus suite : suites) {
-                        res.append("{color:#d04437}").append(suite.name).append("{color}");
-                        res.append(" [[tests ").append(suite.failedTests);
+                fails.forEach((k, v) -> res.addAll(v));
+            }
+        }
 
-                        if (suite.result != null && !suite.result.isEmpty())
-                            res.append(' ').append(suite.result);
+        return res;
+    }
 
-                        res.append('|').append(suite.webToBuild).append("]]\\n");
+    /** */
+    private String generateJiraComment(List<SuiteCurrentStatus> suites, String webUrl) {
+        StringBuilder res = new StringBuilder();
+
+        for (SuiteCurrentStatus suite : suites) {
+            res.append("{color:#d04437}").append(suite.name).append("{color}");
+            res.append(" [[tests ").append(suite.failedTests);
 
-                        for (TestFailure failure : suite.testFailures) {
-                            res.append("* ");
+            if (suite.result != null && !suite.result.isEmpty())
+                res.append(' ').append(suite.result);
 
-                            if (failure.suiteName != null && failure.testName != null)
-                                res.append(failure.suiteName).append(": ").append(failure.testName);
-                            else
-                                res.append(failure.name);
+            res.append('|').append(suite.webToBuild).append("]]\\n");
 
-                            FailureSummary recent = failure.histBaseBranch.recent;
+            for (TestFailure failure : suite.testFailures) {
+                res.append("* ");
 
-                            if (recent != null) {
-                                if (recent.failureRate != null) {
-                                    res.append(" - ").append(recent.failureRate).append("% fails in last ")
-                                        .append(MAX_LATEST_RUNS).append(" master runs.");
-                                }
-                                else if (recent.failures != null && recent.runs != null) {
-                                    res.append(" - ").append(recent.failures).append(" fails / ")
-                                        .append(recent.runs).append(" runs.");
-                                }
-                            }
+                if (failure.suiteName != null && failure.testName != null)
+                    res.append(failure.suiteName).append(": ").append(failure.testName);
+                else
+                    res.append(failure.name);
 
-                            res.append("\\n");
-                        }
+                FailureSummary recent = failure.histBaseBranch.recent;
 
-                        res.append("\\n");
+                if (recent != null) {
+                    if (recent.failureRate != null) {
+                        res.append(" - ").append(recent.failureRate).append("% fails in last ")
+                            .append(MAX_LATEST_RUNS).append(" master runs.");
+                    }
+                    else if (recent.failures != null && recent.runs != null) {
+                        res.append(" - ").append(recent.failures).append(" fails / ")
+                            .append(recent.runs).append(" runs.");
                     }
                 }
 
-                if (res.length() > 0) {
-                    res.insert(0, "{panel:title=Possible Blockers|" +
-                        "borderStyle=dashed|borderColor=#ccc|titleBGColor=#F7D6C1}\\n")
-                        .append("{panel}");
-                }
-                else {
-                    res.append("{panel:title=No blockers found!|" +
-                        "borderStyle=dashed|borderColor=#ccc|titleBGColor=#D6F7C1}{panel}");
-                }
+                res.append("\\n");
             }
+
+            res.append("\\n");
+        }
+
+        if (res.length() > 0) {
+            res.insert(0, "{panel:title=Possible Blockers|" +
+                "borderStyle=dashed|borderColor=#ccc|titleBGColor=#F7D6C1}\\n")
+                .append("{panel}");
+        }
+        else {
+            res.append("{panel:title=No blockers found!|" +
+                "borderStyle=dashed|borderColor=#ccc|titleBGColor=#D6F7C1}{panel}");
         }
 
         res.append("\\n").append("[TeamCity Run All Results|").append(webUrl).append(']');
@@ -248,6 +267,24 @@ else if (recent.failures != null && recent.runs != null) {
         return xmlEscapeText(res.toString());
     }
 
+    /**
+     * @param buildTypeId Suite name.
+     * @param branchForTc Branch for TeamCity.
+     * @param srvId Server id.
+     * @param prov Credentials.
+     * @param webUrl Build URL.
+     * @return Comment, which should be sent to the JIRA ticket.
+     */
+    private String generateJiraComment(
+        String buildTypeId,
+        String branchForTc,
+        String srvId,
+        ICredentialsProv prov,
+        String webUrl
+    ) {
+        return generateJiraComment(getSuitesStatuses(buildTypeId, branchForTc,srvId, prov), webUrl);
+    }
+
     /**
      * @param srv Server.
      * @return Failures for given server.
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/di/IgniteTcBotModule.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/di/IgniteTcBotModule.java
index b2aa2d86..618b681c 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/di/IgniteTcBotModule.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/di/IgniteTcBotModule.java
@@ -42,6 +42,7 @@
 import org.apache.ignite.ci.util.ExceptionUtil;
 import org.apache.ignite.ci.web.BackgroundUpdater;
 import org.apache.ignite.ci.web.TcUpdatePool;
+import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.web.rest.exception.ServiceStartingException;
 
 /**
@@ -90,7 +91,7 @@
     private static class Jira implements IJiraIntegration {
         @Inject ITcHelper helper;
 
-        @Override public String notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
+        @Override public Visa notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
             String ticket) {
             return helper.notifyJira(srvId, prov, buildTypeId, branchForTc, ticket);
         }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
index a20ead4d..aa1084c5 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.ci.jira;
 
+import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.user.ICredentialsProv;
 
 /**
@@ -34,6 +35,6 @@
      * @param ticket JIRA ticket full name. E.g. IGNITE-5555
      * @return {@code True} if JIRA was notified.
      */
-    public String notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
+    public Visa notifyJira(String srvId, ICredentialsProv prov, String buildTypeId, String branchForTc,
         String ticket);
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java
index 1e08eb86..cf294684 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildObserver.java
@@ -23,6 +23,8 @@
 import javax.inject.Inject;
 import org.apache.ignite.ci.tcmodel.result.Build;
 import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.web.model.VisaRequest;
+import org.apache.ignite.ci.web.model.hist.VisasHistoryStorage;
 
 /**
  *
@@ -37,6 +39,9 @@
     /** Task, which should be done periodically. */
     private ObserverTask observerTask;
 
+    /** Visas History Storage. */
+    @Inject private VisasHistoryStorage visasStorage;
+
     /**
      */
     @Inject
@@ -58,10 +63,15 @@ public void stop() {
     /**
      * @param srvId Server id.
      * @param prov Credentials.
+     * @param branchForTc Branch for TC.
      * @param ticket JIRA ticket name.
      */
-    public void observe(String srvId, ICredentialsProv prov, String ticket, Build... builds) {
-        observerTask.addBuild(new BuildsInfo(srvId, prov, ticket, builds));
+    public void observe(String srvId, ICredentialsProv prov, String ticket, String branchForTc, Build... builds) {
+        BuildsInfo buildsInfo = new BuildsInfo(srvId, prov, ticket, branchForTc, builds);
+
+        visasStorage.put(new VisaRequest(buildsInfo));
+
+        observerTask.addInfo(buildsInfo);
     }
 
     /**
@@ -70,10 +80,11 @@ public void observe(String srvId, ICredentialsProv prov, String ticket, Build...
      */
     public String getObservationStatus(String srvId, String branch) {
         StringBuilder sb = new StringBuilder();
-        Collection<BuildsInfo> builds = observerTask.getBuilds();
+
+        Collection<BuildsInfo> builds = observerTask.getInfos();
 
         for (BuildsInfo bi : builds) {
-            if (Objects.equals(bi.branchName, branch)
+            if (Objects.equals(bi.branchForTc, branch)
                 && Objects.equals(bi.srvId, srvId)) {
                 sb.append(bi.ticket).append(" to be commented, waiting for builds. ");
                 sb.append(bi.finishedBuildsCount());
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java
index ba8b48ab..d3b17c02 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/BuildsInfo.java
@@ -17,17 +17,34 @@
 
 package org.apache.ignite.ci.observer;
 
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
 import org.apache.ignite.ci.tcmodel.result.Build;
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
 import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.web.model.ContributionKey;
 
 /**
  *
  */
 public class BuildsInfo {
+    /** */
+    public static final String FINISHED_STATE = "finished";
+
+    /** */
+    public static final String RUNNING_STATE = "running";
+
+    /** */
+    public static final String FINISHED_WITH_FAILURES_STATE = "finished with failures";
+
+    /** */
+    public final String userName;
+
     /** Server id. */
     public final String srvId;
 
@@ -35,42 +52,85 @@
     public final String buildTypeId;
 
     /** Branch name. */
-    public final String branchName;
+    public final String branchForTc;
 
     /** JIRA ticket full name. */
     public final String ticket;
 
+    /** */
+    public final Date date;
+
     /** Finished builds. */
-    private final Map<Build, Boolean> finishedBuilds = new HashMap<>();
+    private final Map<Integer, Boolean> finishedBuilds = new HashMap<>();
+
+    /** */
+    public BuildsInfo(CompactBuildsInfo buildsInfo, IgniteStringCompactor strCompactor) {
+        this.userName = strCompactor.getStringFromId(buildsInfo.userName);
+        this.date = buildsInfo.date;
+        this.srvId = strCompactor.getStringFromId(buildsInfo.srvId);
+        this.ticket = strCompactor.getStringFromId(buildsInfo.ticket);
+        this.branchForTc = strCompactor.getStringFromId(buildsInfo.branchForTc);
+        this.buildTypeId = strCompactor.getStringFromId(buildsInfo.buildTypeId);
+        this.finishedBuilds.putAll(buildsInfo.getFinishedBuilds());
+    }
 
     /**
      * @param srvId Server id.
      * @param prov Prov.
+     * @param branchForTc Branch for TC.
      * @param ticket Ticket.
      * @param builds Builds.
      */
-    public BuildsInfo(String srvId, ICredentialsProv prov, String ticket, Build[] builds) {
+    public BuildsInfo(String srvId, ICredentialsProv prov, String ticket, String branchForTc, Build... builds) {
+        this.userName = prov.getUser(srvId);
+        this.date = Calendar.getInstance().getTime();
         this.srvId = srvId;
         this.ticket = ticket;
-        this.buildTypeId = builds.length > 1 ? "IgniteTests24Java8_RunAll" : builds[0].buildTypeId;
-        this.branchName = builds[0].branchName;
+        this.branchForTc = branchForTc;
+        this.buildTypeId = builds.length == 1 ? builds[0].buildTypeId : "IgniteTests24Java8_RunAll";
 
         for (Build build : builds)
-            finishedBuilds.put(build, false);
+            finishedBuilds.put(build.getId(), false);
     }
 
     /**
      * @param teamcity Teamcity.
      */
-    public boolean isFinished(IAnalyticsEnabledTeamcity teamcity) {
-        for (Map.Entry<Build, Boolean> entry : finishedBuilds.entrySet()) {
+    public String getState(IAnalyticsEnabledTeamcity teamcity) {
+        for (Map.Entry<Integer, Boolean> entry : finishedBuilds.entrySet()) {
+            if (entry.getValue() == null)
+                return FINISHED_WITH_FAILURES_STATE;
+
             if (!entry.getValue()) {
-                Build build = teamcity.getBuild(entry.getKey().getId());
-                entry.setValue(build.isFinished());
+                Build build = teamcity.getBuild(entry.getKey());
+
+                if (build.isFinished()) {
+                    if (build.isUnknown()) {
+                        entry.setValue(null);
+
+                        return FINISHED_WITH_FAILURES_STATE;
+                    }
+
+                    entry.setValue(true);
+                }
             }
         }
 
-        return !finishedBuilds.containsValue(false);
+        return finishedBuilds.containsValue(false) ? RUNNING_STATE : FINISHED_STATE;
+    }
+
+    /**
+     * @param teamcity Teamcity.
+     */
+    public boolean isFinished(IAnalyticsEnabledTeamcity teamcity) {
+        return FINISHED_STATE.equals(getState(teamcity));
+    }
+
+    /**
+     * @param teamcity Teamcity.
+     */
+    public boolean isFinishedWithFailures(IAnalyticsEnabledTeamcity teamcity) {
+        return FINISHED_WITH_FAILURES_STATE.equals(getState(teamcity));
     }
 
     /**
@@ -87,6 +147,16 @@ public int finishedBuildsCount(){
         return (int)finishedBuilds.values().stream().filter(v -> v).count();
     }
 
+    /** */
+    public ContributionKey getContributionKey() {
+        return new ContributionKey(srvId, ticket, branchForTc);
+    }
+
+    /** */
+    public Map<Integer, Boolean> getBuilds() {
+        return Collections.unmodifiableMap(finishedBuilds);
+    }
+
     /** {@inheritDoc} */
     @Override public boolean equals(Object o) {
         if (this == o)
@@ -99,13 +169,14 @@ public int finishedBuildsCount(){
 
         return Objects.equals(srvId, info.srvId) &&
             Objects.equals(buildTypeId, info.buildTypeId) &&
-            Objects.equals(branchName, info.branchName) &&
+            Objects.equals(branchForTc, info.branchForTc) &&
             Objects.equals(ticket, info.ticket) &&
-            Objects.equals(finishedBuilds.keySet(), info.finishedBuilds.keySet());
+            Objects.equals(finishedBuilds.keySet(), info.finishedBuilds.keySet()) &&
+            Objects.equals(date, info.date);
     }
 
     /** {@inheritDoc} */
     @Override public int hashCode() {
-        return Objects.hash(srvId, buildTypeId, branchName, ticket, finishedBuilds.keySet());
+        return Objects.hash(srvId, buildTypeId, branchForTc, ticket, finishedBuilds.keySet(), date);
     }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/CompactBuildsInfo.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/CompactBuildsInfo.java
new file mode 100644
index 00000000..6bab08d0
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/CompactBuildsInfo.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.observer;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
+
+/**
+ *
+ */
+public class CompactBuildsInfo {
+    /** */
+    public final int userName;
+
+    /** Server id. */
+    public final int srvId;
+
+    /** Build type id. */
+    public final int buildTypeId;
+
+    /** Branch name. */
+    public final int branchForTc;
+
+    /** JIRA ticket full name. */
+    public final int ticket;
+
+    /** */
+    public final Date date;
+
+    /** Finished builds. */
+    private final Map<Integer, Boolean> finishedBuilds = new HashMap<>();
+
+    /** */
+    public CompactBuildsInfo(BuildsInfo buildsInfo, IgniteStringCompactor strCompactor) {
+        this.userName = strCompactor.getStringId(buildsInfo.userName);
+        this.date = buildsInfo.date;
+        this.srvId = strCompactor.getStringId(buildsInfo.srvId);
+        this.ticket = strCompactor.getStringId(buildsInfo.ticket);
+        this.branchForTc = strCompactor.getStringId(buildsInfo.branchForTc);
+        this.buildTypeId = strCompactor.getStringId(buildsInfo.buildTypeId);
+        this.finishedBuilds.putAll(buildsInfo.getBuilds());
+    }
+
+    /** */
+    public Map<Integer, Boolean> getFinishedBuilds() {
+        return Collections.unmodifiableMap(finishedBuilds);
+    }
+
+    /** */
+    public BuildsInfo toBuildInfo(IgniteStringCompactor compactor) {
+        return new BuildsInfo(this, compactor);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (!(o instanceof CompactBuildsInfo))
+            return false;
+
+        CompactBuildsInfo info = (CompactBuildsInfo)o;
+
+        return Objects.equals(srvId, info.srvId) &&
+            Objects.equals(buildTypeId, info.buildTypeId) &&
+            Objects.equals(branchForTc, info.branchForTc) &&
+            Objects.equals(ticket, info.ticket) &&
+            Objects.equals(finishedBuilds.keySet(), info.finishedBuilds.keySet()) &&
+            Objects.equals(date, info.date);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(srvId, buildTypeId, branchForTc, ticket, finishedBuilds.keySet(), date);
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java
index 37d6e0f2..22f00e79 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/observer/ObserverTask.java
@@ -17,26 +17,29 @@
 
 package org.apache.ignite.ci.observer;
 
+import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.HashSet;
-import java.util.Queue;
+import java.util.List;
 import java.util.Set;
 import java.util.TimerTask;
+import javax.cache.Cache;
 import javax.inject.Inject;
 import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
 import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
 import org.apache.ignite.ci.ITcHelper;
+import org.apache.ignite.ci.db.TcHelperDb;
 import org.apache.ignite.ci.di.AutoProfiling;
 import org.apache.ignite.ci.di.MonitoredTask;
 import org.apache.ignite.ci.jira.IJiraIntegration;
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
 import org.apache.ignite.ci.user.ICredentialsProv;
-import org.apache.ignite.configuration.CollectionConfiguration;
+import org.apache.ignite.ci.web.model.Visa;
+import org.apache.ignite.ci.web.model.hist.VisasHistoryStorage;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static org.apache.ignite.ci.jira.IJiraIntegration.JIRA_COMMENTED;
-
 /**
  * Checks observed builds for finished status and comments JIRA ticket.
  */
@@ -44,6 +47,9 @@
     /** Logger. */
     private static final Logger logger = LoggerFactory.getLogger(ObserverTask.class);
 
+    /** */
+    public static final String BUILDS_CACHE_NAME = "compactBuildsInfos";
+
     /** Helper. */
     @Inject private ITcHelper tcHelper;
 
@@ -53,28 +59,34 @@
     /** Ignite. */
     @Inject private Ignite ignite;
 
+    /** */
+    @Inject private VisasHistoryStorage visasHistoryStorage;
+
+    /** */
+    @Inject private IgniteStringCompactor strCompactor;
+
     /**
      */
     ObserverTask() {
     }
 
     /** */
-    private Queue<BuildsInfo> buildsQueue() {
-        CollectionConfiguration cfg = new CollectionConfiguration();
-
-        cfg.setBackups(1);
-
-        return ignite.queue("buildsQueue", 0, cfg);
+    private IgniteCache<CompactBuildsInfo, Object> compactInfos() {
+        return ignite.getOrCreateCache(TcHelperDb.getCacheV2TxConfig(BUILDS_CACHE_NAME));
     }
 
     /** */
-    public Collection<BuildsInfo> getBuilds() {
-        return Collections.unmodifiableCollection(buildsQueue());
+    public Collection<BuildsInfo> getInfos() {
+        List<BuildsInfo> buildsInfos = new ArrayList<>();
+
+        compactInfos().forEach(entry -> buildsInfos.add(entry.getKey().toBuildInfo(strCompactor)));
+
+        return buildsInfos;
     }
 
     /** */
-    public void addBuild(BuildsInfo build) {
-        buildsQueue().add(build);
+    public void addInfo(BuildsInfo info) {
+        compactInfos().put(new CompactBuildsInfo(info, strCompactor), new Object());
     }
 
     /** {@inheritDoc} */
@@ -100,13 +112,25 @@ protected String runObserverTask() {
         int notFinishedBuilds = 0;
         Set<String> ticketsNotified = new HashSet<>();
 
-        Queue<BuildsInfo> builds = buildsQueue();
+        for (Cache.Entry<CompactBuildsInfo, Object> entry : compactInfos()) {
+            CompactBuildsInfo compactInfo = entry.getKey();
+
+            BuildsInfo info = compactInfo.toBuildInfo(strCompactor);
 
-        for (BuildsInfo info : builds) {
             checkedBuilds += info.buildsCount();
 
             IAnalyticsEnabledTeamcity teamcity = tcHelper.server(info.srvId, tcHelper.getServerAuthorizerCreds());
 
+            if (info.isFinishedWithFailures(teamcity)) {
+                compactInfos().remove(compactInfo);
+
+                logger.error("JIRA will not be commented." +
+                    " [ticket: " + info.ticket + ", branch:" + info.branchForTc + "] : " +
+                    "one or more re-runned blocker's builds finished with UNKNOWN status.");
+
+                continue;
+            }
+
             if (!info.isFinished(teamcity)) {
                 notFinishedBuilds += info.buildsCount() - info.finishedBuildsCount();
 
@@ -115,13 +139,15 @@ protected String runObserverTask() {
 
             ICredentialsProv creds = tcHelper.getServerAuthorizerCreds();
 
-            String jiraRes = jiraIntegration.notifyJira(info.srvId, creds, info.buildTypeId,
-                info.branchName, info.ticket);
+            Visa visa = jiraIntegration.notifyJira(info.srvId, creds, info.buildTypeId,
+                info.branchForTc, info.ticket);
+
+            visasHistoryStorage.updateVisaRequestRes(info.getContributionKey(), info.date, visa);
 
-            if (JIRA_COMMENTED.equals(jiraRes)) {
+            if (visa.isSuccess()) {
                 ticketsNotified.add(info.ticket);
 
-                builds.remove(info);
+                compactInfos().remove(compactInfo);
             }
         }
 
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
index ecc42679..7b494b5a 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/TcBotTriggerAndSignOffService.java
@@ -19,6 +19,8 @@
 
 import com.google.common.base.Strings;
 import com.google.inject.Provider;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
@@ -28,6 +30,7 @@
 import javax.inject.Inject;
 import javax.ws.rs.QueryParam;
 import org.apache.ignite.ci.ITcHelper;
+import org.apache.ignite.ci.IAnalyticsEnabledTeamcity;
 import org.apache.ignite.ci.github.GitHubUser;
 import org.apache.ignite.ci.github.PullRequest;
 import org.apache.ignite.ci.github.ignited.IGitHubConnIgnitedProvider;
@@ -36,11 +39,16 @@
 import org.apache.ignite.ci.jira.IJiraIntegration;
 import org.apache.ignite.ci.observer.BuildObserver;
 import org.apache.ignite.ci.tcmodel.hist.BuildRef;
+import org.apache.ignite.ci.observer.BuildsInfo;
 import org.apache.ignite.ci.tcmodel.result.Build;
 import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnited;
 import org.apache.ignite.ci.teamcity.ignited.ITeamcityIgnitedProvider;
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
+import org.apache.ignite.ci.web.model.VisaRequest;
+import org.apache.ignite.ci.web.model.Visa;
 import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.web.model.SimpleResult;
+import org.apache.ignite.ci.web.model.hist.VisasHistoryStorage;
 import org.apache.ignite.internal.util.typedef.F;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -66,14 +74,66 @@
 
     @Inject Provider<BuildObserver> observer;
 
+    /** */
+    @Inject private VisasHistoryStorage visasHistoryStorage;
+
+    /** */
+    @Inject IgniteStringCompactor strCompactor;
+
     /** Helper. */
     @Inject ITcHelper tcHelper;
 
+    /** */
+    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
     /** */
     public void startObserver() {
         buildObserverProvider.get();
     }
 
+    /** */
+    public List<VisaStatus> getVisasStatus(String srvId, ICredentialsProv prov) {
+        List<VisaStatus> visaStatuses = new ArrayList<>();
+
+        IAnalyticsEnabledTeamcity teamcity = tcHelper.server(srvId, prov);
+
+        for (VisaRequest visaRequest : visasHistoryStorage.getVisas()) {
+            VisaStatus visaStatus = new VisaStatus();
+
+            BuildsInfo info = visaRequest.getInfo();
+
+            Visa visa = visaRequest.getResult();
+
+            visaStatus.date = formatter.format(info.date);
+            visaStatus.branchName = info.branchForTc;
+            visaStatus.userName = info.userName;
+            visaStatus.ticket = info.ticket;
+
+            if (info.isFinished(teamcity)) {
+                if (visa.isEmpty())
+                    visaStatus.state = BuildsInfo.FINISHED_STATE + " [ waiting results ]";
+                else if (visa.isSuccess()) {
+                    visaStatus.commentUrl = "https://issues.apache.org/jira/browse/" + visaStatus.ticket +
+                        "?focusedCommentId=" + visa.getJiraCommentResponse().getId() +
+                        "&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-" +
+                        visa.getJiraCommentResponse().getId();
+
+                    visaStatus.blockers = visa.getBlockers();
+
+                    visaStatus.state = BuildsInfo.FINISHED_STATE;
+                }
+                else
+                    visaStatus.state = BuildsInfo.FINISHED_WITH_FAILURES_STATE;
+            }
+            else
+                visaStatus.state = info.getState(teamcity);
+
+            visaStatuses.add(visaStatus);
+        }
+
+        return visaStatuses;
+    }
+
     /**
      * @param pr Pull Request.
      * @return JIRA ticket full name or empty string.
@@ -94,7 +154,6 @@ public void startObserver() {
         return ticketId;
     }
 
-
     @NotNull public String triggerBuildsAndObserve(
         @Nullable String srvId,
         @Nullable String branchForTc,
@@ -161,7 +220,7 @@ private String observeJira(
             ticketFullName = ticketFullName.toUpperCase().startsWith("IGNITE-") ? ticketFullName : "IGNITE-" + ticketFullName;
         }
 
-        buildObserverProvider.get().observe(srvId, prov, ticketFullName, builds);
+        buildObserverProvider.get().observe(srvId, prov, ticketFullName, branchForTc, builds);
 
         if (!tcHelper.isServerAuthorized())
             return "Ask server administrator to authorize the Bot to enable JIRA notifications.";
@@ -211,9 +270,14 @@ public SimpleResult commentJiraEx(
         }
 
         if (!Strings.isNullOrEmpty(ticketFullName)) {
-            jiraRes = jiraIntegration.notifyJira(srvId, prov, suiteId, branchForTc, ticketFullName);
+            BuildsInfo buildsInfo = new BuildsInfo(srvId, prov, ticketFullName, branchForTc);
+
+            Visa visa = jiraIntegration.notifyJira(srvId, prov, suiteId, branchForTc, ticketFullName);
+
+            visasHistoryStorage.put(new VisaRequest(buildsInfo)
+                .setResult(visa));
 
-            return new SimpleResult(jiraRes);
+            return new SimpleResult(visa.status);
         }
         else
             return new SimpleResult("JIRA wasn't commented." + (!jiraRes.isEmpty() ? "<br>" + jiraRes : ""));
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
new file mode 100644
index 00000000..1dfe31fc
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.tcbot.visa;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ *
+ */
+public class VisaStatus {
+    /** */
+    @Nullable public String userName;
+
+    /** Branch name. */
+    @Nullable public String branchName;
+
+    /** JIRA ticket full name. */
+    @Nullable public String ticket;
+
+    /** */
+    @Nullable public String state;
+
+    /** */
+    @Nullable public String commentUrl;
+
+    /** */
+    @Nullable public String date;
+
+    /** */
+    public int blockers;
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java
index 0ac3293e..ee9f7e6d 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcmodel/hist/BuildRef.java
@@ -187,4 +187,9 @@ public boolean isQueued() {
     public boolean isRunning() {
         return STATE_RUNNING.equals(state());
     }
+
+    /** */
+    public boolean isUnknown() {
+        return STATUS_UNKNOWN.equals(status());
+    }
 }
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactContributionKey.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactContributionKey.java
new file mode 100644
index 00000000..2d783fdf
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactContributionKey.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model;
+
+import java.util.Objects;
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
+
+/**
+ *
+ */
+public class CompactContributionKey {
+    /** */
+    public final int srvId;
+
+    /** */
+    public final int ticket;
+
+    /** */
+    public final int branchForTc;
+
+    /** */
+    public CompactContributionKey(ContributionKey key, IgniteStringCompactor strCompactor) {
+        this.branchForTc = strCompactor.getStringId(key.branchForTc);
+        this.srvId = strCompactor.getStringId(key.srvId);
+        this.ticket = strCompactor.getStringId(key.ticket);
+    }
+
+    /** {@inheritDoc} */
+    @Override public boolean equals(Object o) {
+        if (this == o)
+            return true;
+
+        if (!(o instanceof CompactContributionKey))
+            return false;
+
+        CompactContributionKey key = (CompactContributionKey)o;
+
+        return Objects.equals(srvId, key.srvId) &&
+            Objects.equals(ticket, key.ticket) &&
+            Objects.equals(branchForTc, key.branchForTc);
+    }
+
+    /** {@inheritDoc} */
+    @Override public int hashCode() {
+        return Objects.hash(srvId, branchForTc, ticket);
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisa.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisa.java
new file mode 100644
index 00000000..9d67f077
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisa.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model;
+
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ *
+ */
+public class CompactVisa {
+    /** */
+    public final int status;
+
+    /** */
+    @Nullable public final JiraCommentResponse jiraCommentRes;
+
+    /** */
+    public final int blockers;
+
+    /** */
+    public CompactVisa(Visa visa, IgniteStringCompactor strCompactor) {
+        this.status = strCompactor.getStringId(visa.status);
+        this.blockers = visa.blockers;
+        this.jiraCommentRes = visa.getJiraCommentResponse();
+    }
+
+    /** */
+    public Visa toVisa(IgniteStringCompactor strCompactor) {
+        return new Visa(strCompactor.getStringFromId(status), jiraCommentRes, blockers);
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisaRequest.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisaRequest.java
new file mode 100644
index 00000000..1fcc59f6
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisaRequest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model;
+
+import org.apache.ignite.ci.observer.CompactBuildsInfo;
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
+
+/**
+ *
+ */
+public class CompactVisaRequest {
+    /** */
+    public final CompactVisa compactVisa;
+
+    /** */
+    public final CompactBuildsInfo compactInfo;
+
+    /** */
+    public CompactVisaRequest(VisaRequest visaReq, IgniteStringCompactor strCompactor) {
+        compactInfo = new CompactBuildsInfo(visaReq.getInfo(), strCompactor);
+
+        compactVisa = new CompactVisa(visaReq.getResult(), strCompactor);
+    }
+
+    /** */
+    public VisaRequest toVisaRequest(IgniteStringCompactor strCompactor) {
+        return new VisaRequest(compactInfo.toBuildInfo(strCompactor)).setResult(compactVisa.toVisa(strCompactor));
+    }
+
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/ContributionKey.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/ContributionKey.java
new file mode 100644
index 00000000..fa62ea64
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/ContributionKey.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model;
+
+/**
+ *
+ */
+public class ContributionKey {
+    /** */
+    public final String srvId;
+
+    /** */
+    public final String ticket;
+
+    /** */
+    public final String branchForTc;
+
+    /** */
+    public ContributionKey(String srvId, String ticket, String branchForTc) {
+        this.branchForTc = branchForTc;
+        this.srvId = srvId;
+        this.ticket = ticket;
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/JiraCommentResponse.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/JiraCommentResponse.java
new file mode 100644
index 00000000..26bde0ae
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/JiraCommentResponse.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ *
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class JiraCommentResponse {
+    /** */
+    private int id;
+
+    /** */
+    public int getId() {
+        return id;
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java
new file mode 100644
index 00000000..4d234baf
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/Visa.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model;
+
+import org.apache.ignite.ci.jira.IJiraIntegration;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ *
+ */
+public class Visa {
+    /** */
+    public static final String EMPTY_VISA_STATUS = "emptyVisa";
+
+    /** */
+    public final String status;
+
+    /** */
+    @Nullable public final JiraCommentResponse jiraCommentRes;
+
+    /** */
+    public final int blockers;
+
+    /** */
+    public static Visa emptyVisa() {
+        return new Visa(EMPTY_VISA_STATUS);
+    }
+
+    /** */
+    public Visa(String status) {
+        this.status = status;
+        this.jiraCommentRes = null;
+        this.blockers = 0;
+    }
+
+    /** */
+    public Visa(String status, JiraCommentResponse res, Integer blockers) {
+        this.status = status;
+        this.jiraCommentRes = res;
+        this.blockers = blockers;
+    }
+
+    /** */
+    @Nullable public JiraCommentResponse getJiraCommentResponse() {
+        return jiraCommentRes;
+    }
+
+    /** */
+    @Nullable public int getBlockers() {
+        return blockers;
+    }
+
+    /** */
+    public boolean isSuccess() {
+        return IJiraIntegration.JIRA_COMMENTED.equals(status)
+            && jiraCommentRes != null;
+    }
+
+    /** */
+    public boolean isEmpty() {
+        return EMPTY_VISA_STATUS.equals(status);
+    }
+
+
+    /** */
+    @Override public String toString() {
+        return status;
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/VisaRequest.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/VisaRequest.java
new file mode 100644
index 00000000..c281a656
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/VisaRequest.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model;
+
+import org.apache.ignite.ci.observer.BuildsInfo;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ *
+ */
+public class VisaRequest {
+    /** */
+    private BuildsInfo info;
+
+    /** */
+    private Visa visa;
+
+    /** */
+    public VisaRequest(BuildsInfo info) {
+        this.info = info;
+        this.visa = Visa.emptyVisa();
+    }
+
+    /** */
+    @Nullable public BuildsInfo getInfo() {
+        return info;
+    }
+
+    /** */
+    @Nullable public Visa getResult() {
+        return visa;
+    }
+
+    /** */
+    public VisaRequest setResult(Visa res) {
+        this.visa = res;
+
+        return this;
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java
new file mode 100644
index 00000000..842d86df
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/hist/VisasHistoryStorage.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.ci.web.model.hist;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import javax.cache.Cache;
+import javax.inject.Inject;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.ci.db.TcHelperDb;
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
+import org.apache.ignite.ci.web.model.CompactContributionKey;
+import org.apache.ignite.ci.web.model.CompactVisaRequest;
+import org.apache.ignite.ci.web.model.ContributionKey;
+import org.apache.ignite.ci.web.model.Visa;
+import org.apache.ignite.ci.web.model.VisaRequest;
+
+/**
+ *
+ */
+public class VisasHistoryStorage {
+    /** */
+    private static final String VISAS_CACHE_NAME = "visasCompactCache";
+
+    /** */
+    @Inject
+    private IgniteStringCompactor strCompactor;
+
+    /** */
+    @Inject
+    private Ignite ignite;
+
+    /** */
+    public void clear() {
+        visas().clear();
+    }
+
+    /** */
+    private Cache<CompactContributionKey, Map<Date, CompactVisaRequest>> visas() {
+        return ignite.getOrCreateCache(TcHelperDb.getCacheV2TxConfig(VISAS_CACHE_NAME));
+    }
+
+    /** */
+    public void put(VisaRequest visaReq) {
+        CompactVisaRequest compactVisaReq = new CompactVisaRequest(visaReq, strCompactor);
+
+        CompactContributionKey key = new CompactContributionKey(new ContributionKey(
+            visaReq.getInfo().srvId,
+            visaReq.getInfo().ticket,
+            visaReq.getInfo().branchForTc), strCompactor);
+
+        Map<Date, CompactVisaRequest> contributionVisas = visas().get(key);
+
+        if (contributionVisas == null)
+            contributionVisas = new HashMap<>();
+
+        contributionVisas.put(compactVisaReq.compactInfo.date, compactVisaReq);
+
+        visas().put(key, contributionVisas);
+    }
+
+    /** */
+    public VisaRequest getVisaReq(ContributionKey key, Date date) {
+        Map<Date, CompactVisaRequest> reqs = visas().get(new CompactContributionKey(key, strCompactor));
+
+        if (Objects.isNull(reqs))
+            return null;
+
+        return reqs.get(date).toVisaRequest(strCompactor);
+    }
+
+    /** */
+    public boolean updateVisaRequestRes(ContributionKey key, Date date, Visa visa) {
+        VisaRequest req = getVisaReq(key, date);
+
+        if (req == null)
+            return false;
+
+        req.setResult(visa);
+
+        put(req);
+
+        return true;
+    }
+
+    /** */
+    public Collection<VisaRequest> getVisas() {
+        List<VisaRequest> res = new ArrayList<>();
+
+        visas().forEach(entry -> res.addAll(entry.getValue().values().stream()
+            .map(v -> v.toVisaRequest(strCompactor))
+            .collect(Collectors.toList())));
+
+        return Collections.unmodifiableCollection(res);
+    }
+}
diff --git a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java
index 5bc1cbf9..5b52dfa3 100644
--- a/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java
+++ b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/rest/visa/TcBotVisaService.java
@@ -16,6 +16,7 @@
  */
 package org.apache.ignite.ci.web.rest.visa;
 
+import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nonnull;
 import javax.servlet.ServletContext;
@@ -27,11 +28,11 @@
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import org.apache.ignite.ci.tcbot.visa.ContributionCheckStatus;
-import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.tcbot.visa.ContributionToCheck;
 import org.apache.ignite.ci.tcbot.visa.TcBotTriggerAndSignOffService;
+import org.apache.ignite.ci.tcbot.visa.VisaStatus;
+import org.apache.ignite.ci.user.ICredentialsProv;
 import org.apache.ignite.ci.web.CtxListener;
-import org.apache.ignite.ci.web.model.SimpleResult;
 import org.apache.ignite.ci.web.rest.exception.ServiceUnauthorizedException;
 import org.jetbrains.annotations.Nullable;
 
@@ -46,6 +47,17 @@
     @Context
     private HttpServletRequest req;
 
+    /**
+     * @param srvId Server id.
+     */
+    @GET
+    @Path("history")
+    public Collection<VisaStatus> history(@Nullable @QueryParam("serverId") String srvId) {
+        return CtxListener.getInjector(ctx)
+            .getInstance(TcBotTriggerAndSignOffService.class)
+            .getVisasStatus(srvId, ICredentialsProv.get(req));
+    }
+
     /**
      * @param srvId Server id.
      */
diff --git a/ignite-tc-helper-web/src/main/webapp/visas.html b/ignite-tc-helper-web/src/main/webapp/visas.html
new file mode 100644
index 00000000..87c1864b
--- /dev/null
+++ b/ignite-tc-helper-web/src/main/webapp/visas.html
@@ -0,0 +1,150 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8">
+        <title>Ignite Teamcity - Visas history</title>
+        <link rel="icon" href="img/leaf-icon-png-7066.png">
+        <script src="https://code.jquery.com/jquery-1.12.4.js"></script>
+        <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
+        <script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
+        <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
+        <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
+        <link rel="stylesheet" href="css/style-1.5.css">
+        <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
+        <script src="js/common-1.6.js"></script>
+        <script src="https://d3js.org/d3.v4.min.js"></script>
+        <script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.js"></script>
+        <script src="https://cdn.datatables.net/1.10.16/js/dataTables.jqueryui.js"></script>
+        <link rel="stylesheet" href="https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
+        <link rel="stylesheet" href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css">
+    </head>
+<body>
+    <br>
+    <div id="loadStatus"></div>
+    <br>
+    <table id="visasTable" class="cell-border" style="width:100%">
+        <thead>
+            <tr class="ui-widget-header ">
+                <th>.</th>
+                <th>.</th>
+                <th>.</th>
+                <th>.</th>
+                <th>.</th>
+                <th>.</th>
+            </tr>
+        </thead>
+    </table>
+    <br>
+    <div id="version"></div>
+<script>
+function showErrInLoadStatus(jqXHR, exception) {
+    if (jqXHR.status === 0) {
+        $("#loadStatus").html('Not connect.\n Verify Network.');
+    } else if (jqXHR.status === 404) {
+        $("#loadStatus").html('Requested page not found. [404]');
+    } else if (jqXHR.status === 401) {
+        $("#loadStatus").html('Unauthorized [401]');
+
+        setTimeout(function() {
+            window.location.href = "/login.html" + "?backref=" + encodeURIComponent(window.location.href);
+        }, 1000);
+    } else if (jqXHR.status === 403) {
+        $("#loadStatus").html('Forbidden [403]');
+    } else if( jqXHR.status === 418) {
+        $("#loadStatus").html('Services are starting [418], I\'m a teapot');
+    } else if (jqXHR.status === 424) {
+        $("#loadStatus").html('Dependency problem: [424]: ' + jqXHR.responseText);
+    } else if (jqXHR.status === 500) {
+        $("#loadStatus").html('Internal Server Error [500].');
+    } else if (exception === 'parsererror') {
+        $("#loadStatus").html('Requested JSON parse failed.');
+    } else if (exception === 'timeout') {
+        $("#loadStatus").html('Time out error.');
+    } else if (exception === 'abort') {
+        $("#loadStatus").html('Ajax request aborted.');
+    } else {
+        $("#loadStatus").html('Uncaught Error.\n' + jqXHR.responseText);
+    }
+}
+
+$(document).ready(function() {
+    loadData();
+
+    $.ajax({ url: "rest/branches/version",  success: showVersionInfo, error: showErrInLoadStatus });
+});
+
+function showVisasTable(result) {
+    let visasTable = $('#visasTable');
+
+    visasTable.dataTable().fnDestroy();
+
+    var table = visasTable.DataTable({
+        "order": [[ 1, 'desc' ]],
+        data: result,
+        "iDisplayLength": 30, //rows to be shown by default
+        stateSave: true,
+        columnDefs: [
+            {
+                targets: '_all',
+                className: 'dt-body-center'
+            },
+        ],
+        columns: [
+            {
+                "data": "state",
+                title: "Status",
+                "render": function (data, type, row, meta) {
+                    if (type === 'display') {
+                        if (data ==='finished' && row.commentUrl)
+                            data = "<a href='" + row.commentUrl + "'>" + data + "</a>";
+                    }
+
+                    return data;
+                }
+            },
+            {
+                "data": "date",
+                title: "Date"
+            },
+            {
+                "data": "userName",
+                title: "User"
+            },
+            {
+                "data": "branchName",
+                title: "Branch"
+            },
+            {
+                "data": "ticket",
+                title: "Ticket"
+            },
+            {
+                "data": "blockers",
+                title: "Blockers",
+                "render": function (data, type, row, meta) {
+                    if (type === 'display') {
+                        if (row.state != 'finished')
+                            data = "";
+                    }
+
+                    return data;
+                }
+            }
+
+        ]
+    });
+}
+
+function loadData() {
+    $.ajax({
+            url: "rest/visa/history?serverId=apache",
+            success: function (result) {
+                showVisasTable(result);
+            },
+            error: showErrInLoadStatus
+        }
+    );
+}
+</script>
+</body>
+</html>


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services