You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by dp...@apache.org on 2018/11/10 13:06:02 UTC

[ignite-teamcity-bot] branch master updated: IGNITE-9939 [Tc Bot] Add visas caсhing and monitoring - Fixes #40.

This is an automated email from the ASF dual-hosted git repository.

dpavlov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite-teamcity-bot.git


The following commit(s) were added to refs/heads/master by this push:
     new efdd0c7  IGNITE-9939 [Tc Bot] Add visas caсhing and monitoring - Fixes #40.
efdd0c7 is described below

commit efdd0c7e0dbda0ae05edbf42775330133b1f7180
Author: ololo3000 <pm...@gmail.com>
AuthorDate: Sat Nov 10 16:05:50 2018 +0300

    IGNITE-9939 [Tc Bot] Add visas caсhing and monitoring - Fixes #40.
    
    Signed-off-by: Dmitriy Pavlov <dp...@apache.org>
---
 .../main/java/org/apache/ignite/ci/ITcHelper.java  |   5 +-
 .../main/java/org/apache/ignite/ci/TcHelper.java   | 139 ++++++++++++-------
 .../org/apache/ignite/ci/di/IgniteTcBotModule.java |   3 +-
 .../apache/ignite/ci/jira/IJiraIntegration.java    |   3 +-
 .../apache/ignite/ci/observer/BuildObserver.java   |  19 ++-
 .../org/apache/ignite/ci/observer/BuildsInfo.java  |  99 ++++++++++++--
 .../ignite/ci/observer/CompactBuildsInfo.java      |  95 +++++++++++++
 .../apache/ignite/ci/observer/ObserverTask.java    |  68 +++++++---
 .../tcbot/visa/TcBotTriggerAndSignOffService.java  |  72 +++++++++-
 .../visa/VisaStatus.java}                          |  39 +++---
 .../apache/ignite/ci/tcmodel/hist/BuildRef.java    |   5 +
 .../ci/web/model/CompactContributionKey.java       |  62 +++++++++
 .../model/CompactVisa.java}                        |  38 +++---
 .../model/CompactVisaRequest.java}                 |  38 +++---
 .../model/ContributionKey.java}                    |  32 ++---
 .../model/JiraCommentResponse.java}                |  25 ++--
 .../java/org/apache/ignite/ci/web/model/Visa.java  |  84 ++++++++++++
 .../model/VisaRequest.java}                        |  48 ++++---
 .../ci/web/model/hist/VisasHistoryStorage.java     | 118 ++++++++++++++++
 .../ignite/ci/web/rest/visa/TcBotVisaService.java  |  16 ++-
 .../src/main/webapp/js/common-1.6.js               |   3 +-
 ignite-tc-helper-web/src/main/webapp/visas.html    | 150 +++++++++++++++++++++
 22 files changed, 966 insertions(+), 195 deletions(-)

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 25a5bd0..481ffa1 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
@@ -24,6 +24,7 @@ import org.apache.ignite.ci.issue.IssuesStorage;
 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;
 
 /**
  * Teamcity Bot main interface. This inteface became too huge.
@@ -67,7 +68,7 @@ public interface ITcHelper extends ITcServerProvider {
      * @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 b40ba69..3c56b4e 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 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
 
     @Inject private PrChainsProcessor prChainsProcessor;
 
+    /** */
+    private final ObjectMapper objectMapper;
+
     public TcHelper() {
+        objectMapper = new ObjectMapper();
     }
 
     /** {@inheritDoc} */
@@ -132,7 +139,7 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
     }
 
     /** {@inheritDoc} */
-    @Override public String notifyJira(
+    @Override public Visa notifyJira(
         String srvId,
         ICredentialsProv prov,
         String buildTypeId,
@@ -144,15 +151,23 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
         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 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
 
             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 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
      * @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 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
 
                 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(']');
@@ -249,6 +268,24 @@ public class TcHelper implements ITcHelper, IJiraIntegration {
     }
 
     /**
+     * @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 b2aa2d8..618b681 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.user.ICredentialsProv;
 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 @@ public class IgniteTcBotModule extends AbstractModule {
     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 a20ead4..aa1084c 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 @@ public interface IJiraIntegration {
      * @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 1e08eb8..cf29468 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 java.util.Timer;
 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 @@ public class BuildObserver {
     /** Task, which should be done periodically. */
     private ObserverTask observerTask;
 
+    /** Visas History Storage. */
+    @Inject private VisasHistoryStorage visasStorage;
+
     /**
      */
     @Inject
@@ -58,10 +63,15 @@ public class BuildObserver {
     /**
      * @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 class BuildObserver {
      */
     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 ba8b48a..d3b17c0 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 class BuildsInfo {
     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 class BuildsInfo {
         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 class BuildsInfo {
 
         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 0000000..6bab08d
--- /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 37d6e0f..22f00e7 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 @@ public class ObserverTask extends TimerTask {
     /** 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 @@ public class ObserverTask extends TimerTask {
     /** 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 @@ public class ObserverTask extends TimerTask {
         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 @@ public class ObserverTask extends TimerTask {
 
             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 5c64286..33ed6a0 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 @@ package org.apache.ignite.ci.tcbot.visa;
 
 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.annotation.Nonnull;
 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.github.pure.IGitHubConnectionProvider;
 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 @@ public class TcBotTriggerAndSignOffService {
 
     @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 class TcBotTriggerAndSignOffService {
         return ticketId;
     }
 
-
     @NotNull public String triggerBuildsAndObserve(
         @Nullable String srvId,
         @Nullable String branchForTc,
@@ -161,7 +220,7 @@ public class TcBotTriggerAndSignOffService {
             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 class TcBotTriggerAndSignOffService {
         }
 
         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/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
similarity index 53%
copy from ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
copy to ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/tcbot/visa/VisaStatus.java
index a20ead4..1dfe31f 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/tcbot/visa/VisaStatus.java
@@ -15,25 +15,32 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.ci.jira;
+package org.apache.ignite.ci.tcbot.visa;
 
-import org.apache.ignite.ci.user.ICredentialsProv;
+import org.jetbrains.annotations.Nullable;
 
 /**
  *
  */
-public interface IJiraIntegration {
-    /** Message to show user when JIRA ticket was successfully commented by the Bot. */
-    public static String JIRA_COMMENTED = "JIRA commented.";
-
-    /**
-     * @param srvId TC Server ID to take information about token from.
-     * @param prov Credentials.
-     * @param buildTypeId Suite name.
-     * @param branchForTc Branch for TeamCity.
-     * @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,
-        String ticket);
+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 0ac3293..ee9f7e6 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 class BuildRef extends AbstractRef {
     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 0000000..2d783fd
--- /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/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisa.java
similarity index 52%
copy from ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
copy to ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisa.java
index a20ead4..9d67f07 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/web/model/CompactVisa.java
@@ -15,25 +15,33 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.ci.jira;
+package org.apache.ignite.ci.web.model;
 
-import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
+import org.jetbrains.annotations.Nullable;
 
 /**
  *
  */
-public interface IJiraIntegration {
-    /** Message to show user when JIRA ticket was successfully commented by the Bot. */
-    public static String JIRA_COMMENTED = "JIRA commented.";
+public class CompactVisa {
+    /** */
+    public final int status;
 
-    /**
-     * @param srvId TC Server ID to take information about token from.
-     * @param prov Credentials.
-     * @param buildTypeId Suite name.
-     * @param branchForTc Branch for TeamCity.
-     * @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,
-        String ticket);
+    /** */
+    @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/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisaRequest.java
similarity index 51%
copy from ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
copy to ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/CompactVisaRequest.java
index a20ead4..1fcc59f 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/web/model/CompactVisaRequest.java
@@ -15,25 +15,31 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.ci.jira;
+package org.apache.ignite.ci.web.model;
 
-import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.observer.CompactBuildsInfo;
+import org.apache.ignite.ci.teamcity.ignited.IgniteStringCompactor;
 
 /**
  *
  */
-public interface IJiraIntegration {
-    /** Message to show user when JIRA ticket was successfully commented by the Bot. */
-    public static String JIRA_COMMENTED = "JIRA commented.";
-
-    /**
-     * @param srvId TC Server ID to take information about token from.
-     * @param prov Credentials.
-     * @param buildTypeId Suite name.
-     * @param branchForTc Branch for TeamCity.
-     * @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,
-        String ticket);
+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/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/ContributionKey.java
similarity index 53%
copy from ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
copy to ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/ContributionKey.java
index a20ead4..fa62ea6 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/web/model/ContributionKey.java
@@ -15,25 +15,25 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.ci.jira;
-
-import org.apache.ignite.ci.user.ICredentialsProv;
+package org.apache.ignite.ci.web.model;
 
 /**
  *
  */
-public interface IJiraIntegration {
-    /** Message to show user when JIRA ticket was successfully commented by the Bot. */
-    public static String JIRA_COMMENTED = "JIRA commented.";
+public class ContributionKey {
+    /** */
+    public final String srvId;
+
+    /** */
+    public final String ticket;
+
+    /** */
+    public final String branchForTc;
 
-    /**
-     * @param srvId TC Server ID to take information about token from.
-     * @param prov Credentials.
-     * @param buildTypeId Suite name.
-     * @param branchForTc Branch for TeamCity.
-     * @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,
-        String ticket);
+    /** */
+    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/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/JiraCommentResponse.java
similarity index 53%
copy from ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
copy to ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/JiraCommentResponse.java
index a20ead4..26bde0a 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/web/model/JiraCommentResponse.java
@@ -15,25 +15,20 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.ci.jira;
+package org.apache.ignite.ci.web.model;
 
-import org.apache.ignite.ci.user.ICredentialsProv;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 
 /**
  *
  */
-public interface IJiraIntegration {
-    /** Message to show user when JIRA ticket was successfully commented by the Bot. */
-    public static String JIRA_COMMENTED = "JIRA commented.";
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class JiraCommentResponse {
+    /** */
+    private int id;
 
-    /**
-     * @param srvId TC Server ID to take information about token from.
-     * @param prov Credentials.
-     * @param buildTypeId Suite name.
-     * @param branchForTc Branch for TeamCity.
-     * @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,
-        String ticket);
+    /** */
+    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 0000000..4d234ba
--- /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/jira/IJiraIntegration.java b/ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/VisaRequest.java
similarity index 53%
copy from ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/jira/IJiraIntegration.java
copy to ignite-tc-helper-web/src/main/java/org/apache/ignite/ci/web/model/VisaRequest.java
index a20ead4..c281a65 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/web/model/VisaRequest.java
@@ -15,25 +15,41 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.ci.jira;
+package org.apache.ignite.ci.web.model;
 
-import org.apache.ignite.ci.user.ICredentialsProv;
+import org.apache.ignite.ci.observer.BuildsInfo;
+import org.jetbrains.annotations.Nullable;
 
 /**
  *
  */
-public interface IJiraIntegration {
-    /** Message to show user when JIRA ticket was successfully commented by the Bot. */
-    public static String JIRA_COMMENTED = "JIRA commented.";
-
-    /**
-     * @param srvId TC Server ID to take information about token from.
-     * @param prov Credentials.
-     * @param buildTypeId Suite name.
-     * @param branchForTc Branch for TeamCity.
-     * @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,
-        String ticket);
+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 0000000..842d86d
--- /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 5bc1cbf..5b52dfa 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.QueryParam;
 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;
 
@@ -50,6 +51,17 @@ public class TcBotVisaService {
      * @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.
+     */
+    @GET
     @Path("contributions")
     public List<ContributionToCheck> contributions(@Nullable @QueryParam("serverId") String srvId) {
         if (!ICredentialsProv.get(req).hasAccess(srvId))
diff --git a/ignite-tc-helper-web/src/main/webapp/js/common-1.6.js b/ignite-tc-helper-web/src/main/webapp/js/common-1.6.js
index 5ca9a34..3b5a72c 100644
--- a/ignite-tc-helper-web/src/main/webapp/js/common-1.6.js
+++ b/ignite-tc-helper-web/src/main/webapp/js/common-1.6.js
@@ -132,7 +132,8 @@ function showMenu(menuData) {
         res += "<a href=\"/comparison.html\">Master Trends</a>";
         res += "<a href=\"/compare.html\">Compare builds</a>";
         res += "<a href=\"/issues.html\">Issues history</a>";
-        //uncomment when Visa history is merged: res += "<a href=\"/visas.html\">Visas history</a>";
+        //uncomment when Visa history is merged:
+        res += "<a href=\"/visas.html\">Visas history</a>";
 
 
         res += "<div class='topnav-right'>";
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 0000000..87c1864
--- /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>