commit 2d013aba0b14809d0f2781ad37eac325444388e6 Author: Karsten Loesing karsten.loesing@gmx.net Date: Wed Nov 29 11:01:06 2017 +0100
Add metrics timeline events underneath graphs.
Implements a first version of #24260. --- .../org/torproject/metrics/web/GraphServlet.java | 60 ++++++++++++++ .../java/org/torproject/metrics/web/Metric.java | 11 +++ .../org/torproject/metrics/web/MetricServlet.java | 12 +++ .../main/java/org/torproject/metrics/web/News.java | 94 +++++++++++++++++++++- website/src/main/resources/etc/metrics.json | 15 ++-- website/src/main/resources/web/WEB-INF/graph.jsp | 41 ++++++++++ website/src/main/resources/web/css/style.css | 24 ++---- 7 files changed, 234 insertions(+), 23 deletions(-)
diff --git a/website/src/main/java/org/torproject/metrics/web/GraphServlet.java b/website/src/main/java/org/torproject/metrics/web/GraphServlet.java index b376be5..02f1cee 100644 --- a/website/src/main/java/org/torproject/metrics/web/GraphServlet.java +++ b/website/src/main/java/org/torproject/metrics/web/GraphServlet.java @@ -219,6 +219,66 @@ public class GraphServlet extends MetricServlet { String url = "?" + urlBuilder.toString().substring(5); request.setAttribute("parameters", url); } + if (this.includeRelatedEvents.contains(requestedId)) { + request.setAttribute("includeRelatedEvents", true); + String startParameter = dateFormat.format(defaultStartDate); + String endParameter = dateFormat.format(defaultEndDate); + String countryParameter = "all"; + String eventsParameter = "off"; + if (null != checkedParameters) { + for (Map.Entry<String, String[]> checkedParameter + : checkedParameters.entrySet()) { + switch (checkedParameter.getKey()) { + case "start": + startParameter = checkedParameter.getValue()[0]; + break; + case "end": + endParameter = checkedParameter.getValue()[0]; + break; + case "country": + countryParameter = checkedParameter.getValue()[0]; + break; + case "events": + eventsParameter = checkedParameter.getValue()[0]; + break; + } + } + } + if (!"off".equals(eventsParameter)) { + request.setAttribute("displayEventsNotice", true); + } + List<String> relatedEvents = new ArrayList<>(); + for (News event : this.sortedEvents) { + if (null == event.getStart()) { + /* Skip event without start date. */ + continue; + } + if (event.getStart().compareTo(endParameter) > 0) { + /* Skip event starting after displayed time period. */ + continue; + } + if (null != event.getEnd() + && event.getEnd().compareTo(startParameter) < 0) { + /* Skip multi-day event ending before displayed time period. */ + continue; + } + if (null == event.getEnd() + && event.getStart().compareTo(startParameter) < 0) { + /* Skip single-day event happening before displayed time period. */ + continue; + } + if (!"all".equals(countryParameter) && null != event.getPlaces() + && !event.getPlaces().contains(countryParameter)) { + /* Skip country-specific event for another country than the + * displayed one. */ + continue; + } + /* We could filter by transport or version here, but that's a + * non-trivial task. */ + relatedEvents.add(event.formatAsTableRow()); + } + request.setAttribute("relatedEvents", relatedEvents); + } } request.getRequestDispatcher("WEB-INF/graph.jsp").forward(request, response); diff --git a/website/src/main/java/org/torproject/metrics/web/Metric.java b/website/src/main/java/org/torproject/metrics/web/Metric.java index 321ed1a..50f3978 100644 --- a/website/src/main/java/org/torproject/metrics/web/Metric.java +++ b/website/src/main/java/org/torproject/metrics/web/Metric.java @@ -3,6 +3,9 @@
package org.torproject.metrics.web;
+import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + @SuppressWarnings("checkstyle:membername") public class Metric {
@@ -28,6 +31,10 @@ public class Metric {
private String[] data_column_spec;
+ @Expose + @SerializedName("include_related_events") + private boolean includeRelatedEvents = false; + public String getId() { return this.id; } @@ -71,5 +78,9 @@ public class Metric { public String[] getData() { return this.data; } + + public boolean getIncludeRelatedEvents() { + return this.includeRelatedEvents; + } }
diff --git a/website/src/main/java/org/torproject/metrics/web/MetricServlet.java b/website/src/main/java/org/torproject/metrics/web/MetricServlet.java index 730a767..0b3bb11 100644 --- a/website/src/main/java/org/torproject/metrics/web/MetricServlet.java +++ b/website/src/main/java/org/torproject/metrics/web/MetricServlet.java @@ -3,7 +3,9 @@
package org.torproject.metrics.web;
+import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -33,6 +35,10 @@ public abstract class MetricServlet extends AnyServlet {
protected Map<String, Category> categoriesById = new HashMap<>();
+ protected Set<String> includeRelatedEvents = new HashSet<>(); + + protected List<News> sortedEvents = new ArrayList<>(); + @Override public void init() throws ServletException { super.init(); @@ -59,6 +65,9 @@ public abstract class MetricServlet extends AnyServlet { if (metric.getData() != null) { this.data.put(id, metric.getData()); } + if (metric.getIncludeRelatedEvents()) { + this.includeRelatedEvents.add(id); + } } for (Category category : ContentProvider.getInstance().getCategoriesList()) { @@ -66,6 +75,9 @@ public abstract class MetricServlet extends AnyServlet { this.categoriesById.put(id, category); } } + this.sortedEvents.addAll(ContentProvider.getInstance().getNewsList()); + Collections.sort(this.sortedEvents, + (o1, o2) -> o2.getStart().compareTo(o1.getStart())); } }
diff --git a/website/src/main/java/org/torproject/metrics/web/News.java b/website/src/main/java/org/torproject/metrics/web/News.java index c7630ab..9afa598 100644 --- a/website/src/main/java/org/torproject/metrics/web/News.java +++ b/website/src/main/java/org/torproject/metrics/web/News.java @@ -3,13 +3,19 @@
package org.torproject.metrics.web;
+import org.torproject.metrics.web.graphs.Countries; + +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + public class News {
private String start;
private String end;
- private String[] places; + private List<String> places;
private String[] protocols;
@@ -27,7 +33,7 @@ public class News { return this.end; }
- String[] getPlaces() { + List<String> getPlaces() { return this.places; }
@@ -46,5 +52,89 @@ public class News { boolean isUnknown() { return this.unknown; } + + static SortedMap<String, String> countries; + + static { + countries = new TreeMap<>(); + for (String[] country : Countries.getInstance().getCountryList()) { + countries.put(country[0], country[1]); + } + } + + String formatAsTableRow() { + StringBuilder sb = new StringBuilder(); + sb.append("<tr><td><span class="dates">"); + if (null == this.start) { + /* Invalid event without start date. */ + sb.append("N/A"); + } else if (null == this.end || this.start.equals(this.end)) { + /* Single-day event. */ + sb.append(this.start); + } else { + /* Multi-day event. */ + sb.append(this.start).append(" to ").append(this.end); + } + sb.append("</span></td><td>"); + if (null != this.places) { + boolean appendUnknownCountry = false; + for (String place : this.getPlaces()) { + if (countries.containsKey(place)) { + sb.append(" <span class="label label-warning">") + .append(countries.get(place)).append("</span>"); + } else { + appendUnknownCountry = true; + } + } + if (appendUnknownCountry) { + sb.append(" <span class="label label-warning">" + + "Unknown country</span>"); + } + } + if (null != this.protocols) { + for (String protocol : this.protocols) { + switch (protocol) { + case "relay": + sb.append(" <span class="label label-success">Relays</span>"); + break; + case "bridge": + sb.append(" <span class="label label-primary">Bridges</span>"); + break; + case "<OR>": + sb.append(" <span class="label label-info"><OR></span>"); + break; + default: + sb.append(" <span class="label label-info">").append(protocol) + .append("</span>"); + break; + } + } + } + if (this.unknown) { + sb.append(" <span class="label label-default">Unknown</span>"); + } + sb.append("</td><td>"); + if (null != this.description) { + sb.append(this.description).append("<br/>"); + } + if (null != this.links) { + for (String link : this.links) { + int tagEnd = link.indexOf('>'); + if (tagEnd < 0 || tagEnd + 2 > link.length()) { + continue; + } + sb.append(link, 0, tagEnd); + sb.append(" class="link""); + if (!link.startsWith("<a href="https://metrics.torproject.org/")) { + sb.append(" target="_blank""); + } + sb.append('>') + .append(link.substring(tagEnd + 1, tagEnd + 2).toUpperCase()) + .append(link.substring(tagEnd + 2)); + } + } + sb.append("</td></tr>"); + return sb.toString(); + } }
diff --git a/website/src/main/resources/etc/metrics.json b/website/src/main/resources/etc/metrics.json index b9bd3ad..a3a4918 100644 --- a/website/src/main/resources/etc/metrics.json +++ b/website/src/main/resources/etc/metrics.json @@ -161,7 +161,8 @@ ], "data": [ "clients" - ] + ], + "include_related_events": true }, { "id": "userstats-relay-table", @@ -222,7 +223,8 @@ ], "data": [ "clients" - ] + ], + "include_related_events": true }, { "id": "userstats-bridge-table", @@ -259,7 +261,8 @@ ], "data": [ "clients" - ] + ], + "include_related_events": true }, { "id": "userstats-bridge-combined", @@ -274,7 +277,8 @@ ], "data": [ "userstats-combined" - ] + ], + "include_related_events": true }, { "id": "userstats-bridge-version", @@ -289,7 +293,8 @@ ], "data": [ "clients" - ] + ], + "include_related_events": true }, { "id": "oxford-anonymous-internet", diff --git a/website/src/main/resources/web/WEB-INF/graph.jsp b/website/src/main/resources/web/WEB-INF/graph.jsp index 98f5a21..238f6d5 100644 --- a/website/src/main/resources/web/WEB-INF/graph.jsp +++ b/website/src/main/resources/web/WEB-INF/graph.jsp @@ -169,6 +169,47 @@
</div><!-- col-md-4 --> </div><!-- row --> + + <c:if test="${includeRelatedEvents}"> + <div class="row"> + <div class="col-md-12"> + <h2>Related events</h2> + <p>The following events have been manually collected on + <a href="https://trac.torproject.org/projects/tor/wiki/doc/MetricsTimeline" target="_blank">this wiki page</a> + and might be related to the displayed graph.</p> + <c:if test="${displayEventsNotice}"> + <div class="alert alert-danger"> + <p>The manually collected events in this table do not + necessarily match the automatically generated possible + censorship events shown in the graph.</p> + </div> + </c:if> + <table class="table events"> + <thead> + <tr> + <th class="dates">Dates</th> + <th class="tags">Places/Protocols</th> + <th class="description">Description and Links</th> + </tr> + </thead> + <tbody> + <c:choose> + <c:when test="${empty relatedEvents}"> + <tr><td colspan="3">No events are known that might + be related to the displayed graph.</td></tr> + </c:when> + <c:otherwise> + <c:forEach var="relatedEvent" items="${relatedEvents}"> + ${relatedEvent} + </c:forEach> + </c:otherwise> + </c:choose> + </tbody> + </table> + </div><!-- col-md-12 --> + </div><!-- row --> + </c:if> + </div><!-- tab-pane --> </div><!-- tab-content --> </div><!-- container --> diff --git a/website/src/main/resources/web/css/style.css b/website/src/main/resources/web/css/style.css index 2652faf..1148e6f 100644 --- a/website/src/main/resources/web/css/style.css +++ b/website/src/main/resources/web/css/style.css @@ -475,23 +475,15 @@ a.btn[target="_blank"]:before {
-/* research download tables */ -td, th { - padding-left:0 !important; -} -td a { - padding-right:1em; +/* related events table */ +.events a.link { padding-right:1em; } +.events th.dates { width:20%; } +.events th.tags { width:20%; } +.events th.description { width:60%; } +.events td span.dates { + color: #7d4698; + font-weight: bold; } -th.title { width:34%; } -th.author { width:34%; } -th.date { width:16%; } -th.download { width:16%; } - - -/* tools table */ -th.title-tools { width:15%; } -th.description { width:70%; } -th.link { width:15%; }