mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-09 14:50:56 +00:00
Anonymous Usage Reporting
This commit is contained in:
parent
7454670b0a
commit
f40f3b3b7b
File diff suppressed because one or more lines are too long
@ -98,6 +98,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
|||||||
router.Get("/rest/system", restGetSystem)
|
router.Get("/rest/system", restGetSystem)
|
||||||
router.Get("/rest/errors", restGetErrors)
|
router.Get("/rest/errors", restGetErrors)
|
||||||
router.Get("/rest/discovery", restGetDiscovery)
|
router.Get("/rest/discovery", restGetDiscovery)
|
||||||
|
router.Get("/rest/report", restGetReport)
|
||||||
router.Get("/qr/:text", getQR)
|
router.Get("/qr/:text", getQR)
|
||||||
|
|
||||||
router.Post("/rest/config", restPostConfig)
|
router.Post("/rest/config", restPostConfig)
|
||||||
@ -107,6 +108,8 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
|
|||||||
router.Post("/rest/error", restPostError)
|
router.Post("/rest/error", restPostError)
|
||||||
router.Post("/rest/error/clear", restClearErrors)
|
router.Post("/rest/error/clear", restClearErrors)
|
||||||
router.Post("/rest/discovery/hint", restPostDiscoveryHint)
|
router.Post("/rest/discovery/hint", restPostDiscoveryHint)
|
||||||
|
router.Post("/rest/report/enable", restPostReportEnable)
|
||||||
|
router.Post("/rest/report/disable", restPostReportDisable)
|
||||||
|
|
||||||
mr := martini.New()
|
mr := martini.New()
|
||||||
mr.Use(csrfMiddleware)
|
mr.Use(csrfMiddleware)
|
||||||
@ -195,7 +198,7 @@ func restGetConfig(w http.ResponseWriter) {
|
|||||||
json.NewEncoder(w).Encode(encCfg)
|
json.NewEncoder(w).Encode(encCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restPostConfig(req *http.Request) {
|
func restPostConfig(req *http.Request, m *model.Model) {
|
||||||
var newCfg config.Configuration
|
var newCfg config.Configuration
|
||||||
err := json.NewDecoder(req.Body).Decode(&newCfg)
|
err := json.NewDecoder(req.Body).Decode(&newCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -242,6 +245,29 @@ func restPostConfig(req *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if newCfg.Options.UREnabled && !cfg.Options.UREnabled {
|
||||||
|
// UR was enabled
|
||||||
|
cfg.Options.UREnabled = true
|
||||||
|
cfg.Options.URDeclined = false
|
||||||
|
cfg.Options.URAccepted = usageReportVersion
|
||||||
|
// Set the corresponding options in newCfg so we don't trigger the restart check if this was the only option change
|
||||||
|
newCfg.Options.URDeclined = false
|
||||||
|
newCfg.Options.URAccepted = usageReportVersion
|
||||||
|
sendUsageRport(m)
|
||||||
|
go usageReportingLoop(m)
|
||||||
|
} else if !newCfg.Options.UREnabled && cfg.Options.UREnabled {
|
||||||
|
// UR was disabled
|
||||||
|
cfg.Options.UREnabled = false
|
||||||
|
cfg.Options.URDeclined = true
|
||||||
|
cfg.Options.URAccepted = 0
|
||||||
|
// Set the corresponding options in newCfg so we don't trigger the restart check if this was the only option change
|
||||||
|
newCfg.Options.URDeclined = true
|
||||||
|
newCfg.Options.URAccepted = 0
|
||||||
|
stopUsageReporting()
|
||||||
|
} else {
|
||||||
|
cfg.Options.URDeclined = newCfg.Options.URDeclined
|
||||||
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(cfg.Options, newCfg.Options) {
|
if !reflect.DeepEqual(cfg.Options, newCfg.Options) {
|
||||||
configInSync = false
|
configInSync = false
|
||||||
}
|
}
|
||||||
@ -347,6 +373,10 @@ func restGetDiscovery(w http.ResponseWriter) {
|
|||||||
json.NewEncoder(w).Encode(discoverer.All())
|
json.NewEncoder(w).Encode(discoverer.All())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restGetReport(w http.ResponseWriter, m *model.Model) {
|
||||||
|
json.NewEncoder(w).Encode(reportData(m))
|
||||||
|
}
|
||||||
|
|
||||||
func getQR(w http.ResponseWriter, params martini.Params) {
|
func getQR(w http.ResponseWriter, params martini.Params) {
|
||||||
code, err := qr.Encode(params["text"], qr.M)
|
code, err := qr.Encode(params["text"], qr.M)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -358,6 +388,33 @@ func getQR(w http.ResponseWriter, params martini.Params) {
|
|||||||
w.Write(code.PNG())
|
w.Write(code.PNG())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func restPostReportEnable(m *model.Model) {
|
||||||
|
if cfg.Options.UREnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Options.UREnabled = true
|
||||||
|
cfg.Options.URDeclined = false
|
||||||
|
cfg.Options.URAccepted = usageReportVersion
|
||||||
|
|
||||||
|
go usageReportingLoop(m)
|
||||||
|
sendUsageRport(m)
|
||||||
|
saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
func restPostReportDisable(m *model.Model) {
|
||||||
|
if !cfg.Options.UREnabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Options.UREnabled = false
|
||||||
|
cfg.Options.URDeclined = true
|
||||||
|
cfg.Options.URAccepted = 0
|
||||||
|
|
||||||
|
stopUsageReporting()
|
||||||
|
saveConfig()
|
||||||
|
}
|
||||||
|
|
||||||
func basic(username string, passhash string) http.HandlerFunc {
|
func basic(username string, passhash string) http.HandlerFunc {
|
||||||
return func(res http.ResponseWriter, req *http.Request) {
|
return func(res http.ResponseWriter, req *http.Request) {
|
||||||
if validAPIKey(req.Header.Get("X-API-Key")) {
|
if validAPIKey(req.Header.Get("X-API-Key")) {
|
||||||
|
@ -393,6 +393,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.Options.UREnabled && cfg.Options.URAccepted < usageReportVersion {
|
||||||
|
l.Infoln("Anonymous usage report has changed; revoking acceptance")
|
||||||
|
cfg.Options.UREnabled = false
|
||||||
|
}
|
||||||
|
if cfg.Options.UREnabled {
|
||||||
|
go usageReportingLoop(m)
|
||||||
|
go func() {
|
||||||
|
time.Sleep(10 * time.Minute)
|
||||||
|
sendUsageRport(m)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
<-stop
|
<-stop
|
||||||
l.Okln("Exiting")
|
l.Okln("Exiting")
|
||||||
}
|
}
|
||||||
|
109
cmd/syncthing/usage_report.go
Normal file
109
cmd/syncthing/usage_report.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/calmh/syncthing/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Current version number of the usage report, for acceptance purposes. If
|
||||||
|
// fields are added or changed this integer must be incremented so that users
|
||||||
|
// are prompted for acceptance of the new report.
|
||||||
|
const usageReportVersion = 1
|
||||||
|
|
||||||
|
var stopUsageReportingCh = make(chan struct{})
|
||||||
|
|
||||||
|
func reportData(m *model.Model) map[string]interface{} {
|
||||||
|
res := make(map[string]interface{})
|
||||||
|
res["uniqueID"] = strings.ToLower(certID([]byte(myID)))[:6]
|
||||||
|
res["version"] = Version
|
||||||
|
res["platform"] = runtime.GOOS + "-" + runtime.GOARCH
|
||||||
|
res["numRepos"] = len(cfg.Repositories)
|
||||||
|
res["numNodes"] = len(cfg.Nodes)
|
||||||
|
|
||||||
|
var totFiles, maxFiles int
|
||||||
|
var totBytes, maxBytes int64
|
||||||
|
for _, repo := range cfg.Repositories {
|
||||||
|
files, _, bytes := m.GlobalSize(repo.ID)
|
||||||
|
totFiles += files
|
||||||
|
totBytes += bytes
|
||||||
|
if files > maxFiles {
|
||||||
|
maxFiles = files
|
||||||
|
}
|
||||||
|
if bytes > maxBytes {
|
||||||
|
maxBytes = bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res["totFiles"] = totFiles
|
||||||
|
res["repoMaxFiles"] = maxFiles
|
||||||
|
res["totMiB"] = totBytes / 1024 / 1024
|
||||||
|
res["repoMaxMiB"] = maxBytes / 1024 / 1024
|
||||||
|
|
||||||
|
var mem runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&mem)
|
||||||
|
res["memoryUsageMiB"] = mem.Sys / 1024 / 1024
|
||||||
|
|
||||||
|
var perf float64
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
p := cpuBench()
|
||||||
|
if p > perf {
|
||||||
|
perf = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res["sha256Perf"] = perf
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendUsageRport(m *model.Model) error {
|
||||||
|
d := reportData(m)
|
||||||
|
var b bytes.Buffer
|
||||||
|
json.NewEncoder(&b).Encode(d)
|
||||||
|
_, err := http.Post("https://data.syncthing.net/newdata", "application/json", &b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func usageReportingLoop(m *model.Model) {
|
||||||
|
l.Infoln("Starting usage reporting")
|
||||||
|
t := time.NewTicker(86400 * time.Second)
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stopUsageReportingCh:
|
||||||
|
break loop
|
||||||
|
case <-t.C:
|
||||||
|
sendUsageRport(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
l.Infoln("Stopping usage reporting")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopUsageReporting() {
|
||||||
|
stopUsageReportingCh <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns CPU performance as a measure of single threaded SHA-256 MiB/s
|
||||||
|
func cpuBench() float64 {
|
||||||
|
chunkSize := 100 * 1 << 10
|
||||||
|
h := sha256.New()
|
||||||
|
bs := make([]byte, chunkSize)
|
||||||
|
rand.Reader.Read(bs)
|
||||||
|
|
||||||
|
t0 := time.Now()
|
||||||
|
b := 0
|
||||||
|
for time.Since(t0) < 125*time.Millisecond {
|
||||||
|
h.Write(bs)
|
||||||
|
b += chunkSize
|
||||||
|
}
|
||||||
|
h.Sum(nil)
|
||||||
|
d := time.Since(t0)
|
||||||
|
return float64(int(float64(b)/d.Seconds()/(1<<20)*100)) / 100
|
||||||
|
}
|
@ -157,6 +157,10 @@ type OptionsConfiguration struct {
|
|||||||
StartBrowser bool `xml:"startBrowser" default:"true"`
|
StartBrowser bool `xml:"startBrowser" default:"true"`
|
||||||
UPnPEnabled bool `xml:"upnpEnabled" default:"true"`
|
UPnPEnabled bool `xml:"upnpEnabled" default:"true"`
|
||||||
|
|
||||||
|
UREnabled bool `xml:"urEnabled"` // If true, send usage reporting data
|
||||||
|
URDeclined bool `xml:"urDeclined"` // If true, don't ask again
|
||||||
|
URAccepted int `xml:"urAccepted"` // Accepted usage reporting version
|
||||||
|
|
||||||
Deprecated_ReadOnly bool `xml:"readOnly,omitempty" json:"-"`
|
Deprecated_ReadOnly bool `xml:"readOnly,omitempty" json:"-"`
|
||||||
Deprecated_GUIEnabled bool `xml:"guiEnabled,omitempty" json:"-"`
|
Deprecated_GUIEnabled bool `xml:"guiEnabled,omitempty" json:"-"`
|
||||||
Deprecated_GUIAddress string `xml:"guiAddress,omitempty" json:"-"`
|
Deprecated_GUIAddress string `xml:"guiAddress,omitempty" json:"-"`
|
||||||
|
57
gui/app.js
57
gui/app.js
@ -30,21 +30,24 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
|||||||
$scope.seenError = '';
|
$scope.seenError = '';
|
||||||
$scope.model = {};
|
$scope.model = {};
|
||||||
$scope.repos = {};
|
$scope.repos = {};
|
||||||
|
$scope.reportData = {};
|
||||||
|
$scope.reportPreview = false;
|
||||||
|
|
||||||
// Strings before bools look better
|
// Strings before bools look better
|
||||||
$scope.settings = [
|
$scope.settings = [
|
||||||
{id: 'ListenStr', descr: 'Sync Protocol Listen Addresses', type: 'text', restart: true},
|
{id: 'ListenStr', descr: 'Sync Protocol Listen Addresses', type: 'text'},
|
||||||
{id: 'MaxSendKbps', descr: 'Outgoing Rate Limit (KiB/s)', type: 'number', restart: true},
|
{id: 'MaxSendKbps', descr: 'Outgoing Rate Limit (KiB/s)', type: 'number'},
|
||||||
{id: 'RescanIntervalS', descr: 'Rescan Interval (s)', type: 'number', restart: true},
|
{id: 'RescanIntervalS', descr: 'Rescan Interval (s)', type: 'number'},
|
||||||
{id: 'ReconnectIntervalS', descr: 'Reconnect Interval (s)', type: 'number', restart: true},
|
{id: 'ReconnectIntervalS', descr: 'Reconnect Interval (s)', type: 'number'},
|
||||||
{id: 'ParallelRequests', descr: 'Max Outstanding Requests', type: 'number', restart: true},
|
{id: 'ParallelRequests', descr: 'Max Outstanding Requests', type: 'number'},
|
||||||
{id: 'MaxChangeKbps', descr: 'Max File Change Rate (KiB/s)', type: 'number', restart: true},
|
{id: 'MaxChangeKbps', descr: 'Max File Change Rate (KiB/s)', type: 'number'},
|
||||||
|
|
||||||
{id: 'GlobalAnnEnabled', descr: 'Global Discovery', type: 'bool', restart: true},
|
{id: 'LocalAnnPort', descr: 'Local Discovery Port', type: 'number'},
|
||||||
{id: 'LocalAnnEnabled', descr: 'Local Discovery', type: 'bool', restart: true},
|
{id: 'LocalAnnEnabled', descr: 'Local Discovery', type: 'bool'},
|
||||||
{id: 'LocalAnnPort', descr: 'Local Discovery Port', type: 'number', restart: true},
|
{id: 'GlobalAnnEnabled', descr: 'Global Discovery', type: 'bool'},
|
||||||
{id: 'StartBrowser', descr: 'Start Browser', type: 'bool'},
|
{id: 'StartBrowser', descr: 'Start Browser', type: 'bool'},
|
||||||
{id: 'UPnPEnabled', descr: 'Enable UPnP', type: 'bool'},
|
{id: 'UPnPEnabled', descr: 'Enable UPnP', type: 'bool'},
|
||||||
|
{id: 'UREnabled', descr: 'Anonymous Usage Reporting', type: 'bool'},
|
||||||
];
|
];
|
||||||
|
|
||||||
$scope.guiSettings = [
|
$scope.guiSettings = [
|
||||||
@ -544,11 +547,47 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
|||||||
$scope.repos = repoMap($scope.config.Repositories);
|
$scope.repos = repoMap($scope.config.Repositories);
|
||||||
|
|
||||||
$scope.refresh();
|
$scope.refresh();
|
||||||
|
|
||||||
|
if (!$scope.config.Options.UREnabled && !$scope.config.Options.URDeclined) {
|
||||||
|
// If usage reporting has been neither accepted nor declined,
|
||||||
|
// we want to ask the user to make a choice. But we don't want
|
||||||
|
// to bug them during initial setup, so we set a cookie with
|
||||||
|
// the time of the first visit. When that cookie is present
|
||||||
|
// and the time is more than four hours ago, we ask the
|
||||||
|
// question.
|
||||||
|
|
||||||
|
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
|
||||||
|
if (!firstVisit) {
|
||||||
|
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
|
||||||
|
} else {
|
||||||
|
if (+firstVisit < Date.now() - 4*3600*1000){
|
||||||
|
$('#ur').modal({backdrop: 'static', keyboard: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$http.get(urlbase + '/config/sync').success(function (data) {
|
$http.get(urlbase + '/config/sync').success(function (data) {
|
||||||
$scope.configInSync = data.configInSync;
|
$scope.configInSync = data.configInSync;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$http.get(urlbase + '/report').success(function (data) {
|
||||||
|
$scope.reportData = data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.acceptUR = function () {
|
||||||
|
$scope.config.Options.UREnabled = true;
|
||||||
|
$scope.config.Options.URDeclined = false;
|
||||||
|
$scope.saveConfig();
|
||||||
|
$('#ur').modal('hide');
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.declineUR = function () {
|
||||||
|
$scope.config.Options.UREnabled = false;
|
||||||
|
$scope.config.Options.URDeclined = true;
|
||||||
|
$scope.saveConfig();
|
||||||
|
$('#ur').modal('hide');
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.init();
|
$scope.init();
|
||||||
|
@ -564,7 +564,7 @@ found in the LICENSE file.
|
|||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title"> Settings</h4>
|
<h4 class="modal-title">Settings</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form role="form">
|
<form role="form">
|
||||||
@ -611,6 +611,32 @@ found in the LICENSE file.
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Usage report modal -->
|
||||||
|
|
||||||
|
<div id="ur" class="modal fade">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header alert alert-success">
|
||||||
|
<h4 class="modal-title">Allow Anonymous Usage Reporting?</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>
|
||||||
|
The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The aggregated statistics are publicly available at <a href="https://data.syncthing.net/">https://data.syncthing.net/</a>.
|
||||||
|
</p>
|
||||||
|
<button type="button" class="btn btn-default" ng-show="!reportPreview" ng-click="reportPreview = true">Preview Usage Report</button>
|
||||||
|
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-success" ng-click="acceptUR()"><span class="glyphicon glyphicon-ok"></span> Yes</button>
|
||||||
|
<button type="button" class="btn btn-danger" ng-click="declineUR()"><span class="glyphicon glyphicon-remove"></span> No</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script src="angular.min.js"></script>
|
<script src="angular.min.js"></script>
|
||||||
<script src="jquery-2.0.3.min.js"></script>
|
<script src="jquery-2.0.3.min.js"></script>
|
||||||
|
Loading…
Reference in New Issue
Block a user