added telemetry reports and license registration

This commit is contained in:
sqshq 2019-05-27 21:44:06 -04:00
parent eb2f9949b6
commit f5fdf635f0
9 changed files with 173 additions and 84 deletions

View File

@ -1,17 +1,25 @@
package client package client
import ( import (
"bytes"
"encoding/json"
"errors"
"github.com/sqshq/sampler/metadata" "github.com/sqshq/sampler/metadata"
"io/ioutil"
"net/http" "net/http"
) )
const ( const (
backendUrl = "http://localhost:8080/api/v1" backendUrl = "http://localhost/api/v1"
registrationPath = "/registration" installationPath = "/telemetry/installation"
reportInstallationPath = "/report/installation" statisticsPath = "/telemetry/statistics"
reportCrashPath = "repost/crash" crashPath = "/telemetry/crash"
registrationPath = "/license/registration"
jsonContentType = "application/json"
) )
// Backend client is used to verify license and to send telemetry reports
// for analyses (anonymous usage data statistics and crash reports)
type BackendClient struct { type BackendClient struct {
client http.Client client http.Client
} }
@ -23,13 +31,57 @@ func NewBackendClient() *BackendClient {
} }
func (c *BackendClient) ReportInstallation(statistics *metadata.Statistics) { func (c *BackendClient) ReportInstallation(statistics *metadata.Statistics) {
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(statistics)
if err != nil {
c.ReportCrash(err.Error(), statistics)
}
_, err = http.Post(backendUrl+installationPath, jsonContentType, buf)
if err != nil {
c.ReportCrash(err.Error(), statistics)
}
}
func (c *BackendClient) ReportUsageStatistics(error string, statistics *metadata.Statistics) {
// TODO // TODO
} }
func (c *BackendClient) ReportCrash() { func (c *BackendClient) ReportCrash(error string, statistics *metadata.Statistics) {
// TODO // TODO
} }
func (c *BackendClient) Register(key string) { func (c *BackendClient) RegisterLicenseKey(licenseKey string, statistics *metadata.Statistics) (*metadata.License, error) {
// TODO
req := struct {
LicenseKey string
Statistics *metadata.Statistics
}{
licenseKey,
statistics,
}
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
c.ReportCrash(err.Error(), statistics)
}
response, err := http.Post(
backendUrl+registrationPath, jsonContentType, buf)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
body, _ := ioutil.ReadAll(response.Body)
return nil, errors.New(string(body))
}
var license metadata.License
json.NewDecoder(response.Body).Decode(&license)
return &license, nil
} }

View File

@ -19,12 +19,12 @@ type StatusBar struct {
} }
func NewStatusLine(configFileName string, palette console.Palette, license *metadata.License) *StatusBar { func NewStatusLine(configFileName string, palette console.Palette, license *metadata.License) *StatusBar {
block := *ui.NewBlock() block := *ui.NewBlock()
block.Border = false block.Border = false
text := fmt.Sprintf(" %s %s | ", console.AppTitle, console.AppVersion) text := fmt.Sprintf(" %s %s | ", console.AppTitle, console.AppVersion)
if license == nil || !license.Purchased || !license.Valid { if license == nil || !license.Valid {
text += console.AppLicenseWarning text += console.AppLicenseWarning
} else if license.Username != nil { } else if license.Username != nil {
text += fmt.Sprintf("%s | licensed to %s", configFileName, *license.Username) text += fmt.Sprintf("%s | licensed to %s", configFileName, *license.Username)

View File

@ -27,20 +27,18 @@ func LoadConfig() (*Config, Options) {
_, err := flags.Parse(&opt) _, err := flags.Parse(&opt)
if err != nil { if err != nil {
panic(err) console.Exit("")
} }
if opt.Version == true { if opt.Version == true {
println(console.AppVersion) console.Exit(console.AppVersion)
os.Exit(0)
} }
if opt.ConfigFile == nil && opt.License == nil { if opt.ConfigFile == nil && opt.LicenseKey == nil {
println("Please specify config file using --config flag. Example: sampler --config example.yml") console.Exit("Please specify config file using --config flag. Example: sampler --config example.yml")
os.Exit(0)
} }
if opt.License != nil { if opt.LicenseKey != nil {
return nil, opt return nil, opt
} }

View File

@ -1,8 +1,9 @@
package config package config
type Options struct { type Options struct {
ConfigFile *string `short:"c" long:"config" required:"false" description:"set path to YAML config file"` ConfigFile *string `short:"c" long:"config" required:"true" description:"Path to YAML config file"`
License *string `short:"l" long:"license" required:"false" description:"provide license key. visit www.sampler.dev for details"` LicenseKey *string `short:"l" long:"license" description:"License key. Visit www.sampler.dev for details"`
Environment []string `short:"e" long:"env" required:"false" description:"specify name=value variable to use in script placeholder as $name. This flag takes precedence over the same name variables, specified in config yml"` Environment []string `short:"e" long:"env" description:"Specify name=value variable to use in script placeholder as $name. This flag takes precedence over the same name variables, specified in config yml"`
Version bool `short:"v" long:"version" required:"false" description:"print version"` Version bool `short:"v" long:"version" description:"Print version"`
DisableTelemetry bool `long:"disable-telemetry" description:"Disable anonymous usage statistics and errors to be sent to Sampler online service for analyses"`
} }

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
ui "github.com/gizak/termui/v3" ui "github.com/gizak/termui/v3"
"log" "log"
"os"
"time" "time"
) )
@ -33,10 +34,17 @@ func Init() {
fmt.Printf("\033]0;%s\007", AppTitle) fmt.Printf("\033]0;%s\007", AppTitle)
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
log.Fatalf("failed to initialize termui: %v", err) log.Fatalf("Failed to initialize ui: %v", err)
} }
} }
func Close() { func Close() {
ui.Close() ui.Close()
} }
func Exit(message string) {
if len(message) > 0 {
println(message)
}
os.Exit(0)
}

101
main.go
View File

@ -21,12 +21,40 @@ import (
) )
type Starter struct { type Starter struct {
lout *layout.Layout
player *asset.AudioPlayer player *asset.AudioPlayer
lout *layout.Layout
palette console.Palette
opt config.Options opt config.Options
cfg config.Config cfg config.Config
} }
func (s *Starter) startAll() {
for _, c := range s.cfg.RunCharts {
cpt := runchart.NewRunChart(c, s.palette)
s.start(cpt, cpt.Consumer, c.ComponentConfig, c.Items, c.Triggers)
}
for _, c := range s.cfg.SparkLines {
cpt := sparkline.NewSparkLine(c, s.palette)
s.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers)
}
for _, c := range s.cfg.BarCharts {
cpt := barchart.NewBarChart(c, s.palette)
s.start(cpt, cpt.Consumer, c.ComponentConfig, c.Items, c.Triggers)
}
for _, c := range s.cfg.Gauges {
cpt := gauge.NewGauge(c, s.palette)
s.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Cur, c.Min, c.Max}, c.Triggers)
}
for _, c := range s.cfg.AsciiBoxes {
cpt := asciibox.NewAsciiBox(c, s.palette)
s.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers)
}
for _, c := range s.cfg.TextBoxes {
cpt := textbox.NewTextBox(c, s.palette)
s.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers)
}
}
func (s *Starter) start(drawable ui.Drawable, consumer *data.Consumer, componentConfig config.ComponentConfig, itemsConfig []config.Item, triggersConfig []config.TriggerConfig) { func (s *Starter) start(drawable ui.Drawable, consumer *data.Consumer, componentConfig config.ComponentConfig, itemsConfig []config.Item, triggersConfig []config.TriggerConfig) {
cpt := component.NewComponent(drawable, consumer, componentConfig) cpt := component.NewComponent(drawable, consumer, componentConfig)
triggers := data.NewTriggers(triggersConfig, consumer, s.opt, s.player) triggers := data.NewTriggers(triggersConfig, consumer, s.opt, s.player)
@ -39,6 +67,14 @@ func (s *Starter) start(drawable ui.Drawable, consumer *data.Consumer, component
func main() { func main() {
cfg, opt := config.LoadConfig() cfg, opt := config.LoadConfig()
bc := client.NewBackendClient()
statistics := metadata.GetStatistics(cfg)
license := metadata.GetLicense()
if opt.LicenseKey != nil {
registerLicense(statistics, opt, bc)
}
console.Init() console.Init()
defer console.Close() defer console.Close()
@ -46,58 +82,35 @@ func main() {
player := asset.NewAudioPlayer() player := asset.NewAudioPlayer()
defer player.Close() defer player.Close()
license := metadata.GetLicense()
statistics := metadata.PersistStatistics(cfg)
if opt.License != nil {
// validate license
// save to storage on success
// exit with info
return
}
palette := console.GetPalette(*cfg.Theme) palette := console.GetPalette(*cfg.Theme)
lout := layout.NewLayout(component.NewStatusLine(*opt.ConfigFile, palette, license), component.NewMenu(palette), component.NewIntro(palette)) lout := layout.NewLayout(
bc := client.NewBackendClient() component.NewStatusLine(*opt.ConfigFile, palette, license), component.NewMenu(palette), component.NewIntro(palette))
if license == nil { if statistics.LaunchCount == 0 {
if !opt.DisableTelemetry {
go bc.ReportInstallation(statistics)
}
lout.RunIntro() lout.RunIntro()
metadata.InitLicense() } else /* with random */ {
bc.ReportInstallation(statistics) // TODO if license == nil lout.showNagWindow() with timeout and OK button
} else if !license.Purchased /* && random */ { // TODO if license != nil, verify license
// TODO lout.showNagWindow() with timeout and OK button // TODO report statistics
} }
starter := &Starter{lout, player, opt, *cfg} metadata.PersistStatistics(cfg)
startComponents(starter, cfg, palette) starter := &Starter{player, lout, palette, opt, *cfg}
starter.startAll()
handler := event.NewHandler(lout, opt) handler := event.NewHandler(lout, opt)
handler.HandleEvents() handler.HandleEvents()
} }
func startComponents(starter *Starter, cfg *config.Config, palette console.Palette) { func registerLicense(statistics *metadata.Statistics, opt config.Options, bc *client.BackendClient) {
for _, c := range cfg.RunCharts { lc, err := bc.RegisterLicenseKey(*opt.LicenseKey, statistics)
cpt := runchart.NewRunChart(c, palette) if err != nil {
starter.start(cpt, cpt.Consumer, c.ComponentConfig, c.Items, c.Triggers) console.Exit("License registration failed: " + err.Error())
} } else {
for _, c := range cfg.SparkLines { metadata.SaveLicense(*lc)
cpt := sparkline.NewSparkLine(c, palette) console.Exit("License successfully verified, Sampler can be restarted without --license flag now. Thank you.")
starter.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers)
}
for _, c := range cfg.BarCharts {
cpt := barchart.NewBarChart(c, palette)
starter.start(cpt, cpt.Consumer, c.ComponentConfig, c.Items, c.Triggers)
}
for _, c := range cfg.Gauges {
cpt := gauge.NewGauge(c, palette)
starter.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Cur, c.Min, c.Max}, c.Triggers)
}
for _, c := range cfg.AsciiBoxes {
cpt := asciibox.NewAsciiBox(c, palette)
starter.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers)
}
for _, c := range cfg.TextBoxes {
cpt := textbox.NewTextBox(c, palette)
starter.start(cpt, cpt.Consumer, c.ComponentConfig, []config.Item{c.Item}, c.Triggers)
} }
} }

View File

@ -6,11 +6,10 @@ import (
) )
type License struct { type License struct {
Purchased bool Key *string `yaml:"k"`
Valid bool Username *string `yaml:"u"`
Key *string Company *string `yaml:"c"`
Username *string Valid bool `yaml:"v"`
Company *string
} }
const licenseFileName = "license.yml" const licenseFileName = "license.yml"
@ -32,18 +31,12 @@ func GetLicense() *License {
} }
} }
func InitLicense() { func SaveLicense(license License) {
license := License{
Purchased: false,
Valid: false,
}
file, err := yaml.Marshal(license) file, err := yaml.Marshal(license)
if err != nil { if err != nil {
log.Fatalf("Failed to marshal config file: %v", err) log.Fatalf("Failed to marshal license file: %v", err)
} }
initStorage() saveStorageFile(file, licenseFileName)
saveStorageFile(file, getPlatformStoragePath(licenseFileName))
} }

View File

@ -9,6 +9,8 @@ import (
"runtime" "runtime"
) )
// Anonymous usage data, which we collect for analyses and improvements
// User can disable it, along with crash reports, using --telemetry flag
type Statistics struct { type Statistics struct {
Version string Version string
OS string OS string
@ -38,7 +40,7 @@ func PersistStatistics(config *config.Config) *Statistics {
} }
statistics.WindowWidth = w statistics.WindowWidth = w
statistics.WindowWidth = h statistics.WindowHeight = h
statistics.LaunchCount += 1 statistics.LaunchCount += 1
} else { } else {
@ -50,6 +52,7 @@ func PersistStatistics(config *config.Config) *Statistics {
WindowHeight: h, WindowHeight: h,
ComponentsCount: countComponentsPerType(config), ComponentsCount: countComponentsPerType(config),
} }
initStorage()
} }
file, err := yaml.Marshal(statistics) file, err := yaml.Marshal(statistics)
@ -62,6 +65,27 @@ func PersistStatistics(config *config.Config) *Statistics {
return statistics return statistics
} }
func GetStatistics(cfg *config.Config) *Statistics {
if !fileExists(statisticsFileName) {
return &Statistics{
Version: console.AppVersion,
OS: runtime.GOOS,
LaunchCount: 0,
WindowWidth: 0,
WindowHeight: 0,
ComponentsCount: countComponentsPerType(cfg),
}
} else {
file := readStorageFile(getPlatformStoragePath(statisticsFileName))
license := new(Statistics)
err := yaml.Unmarshal(file, license)
if err != nil {
log.Fatalf("Failed to read statistics file: %v", err)
}
return license
}
}
func countComponentsPerType(config *config.Config) map[string]int { func countComponentsPerType(config *config.Config) map[string]int {
m := make(map[string]int) m := make(map[string]int)

View File

@ -51,6 +51,6 @@ func readStorageFile(path string) []byte {
func saveStorageFile(file []byte, fileName string) { func saveStorageFile(file []byte, fileName string) {
err := ioutil.WriteFile(getPlatformStoragePath(fileName), file, os.ModePerm) err := ioutil.WriteFile(getPlatformStoragePath(fileName), file, os.ModePerm)
if err != nil { if err != nil {
log.Fatalf("Failed to save the storage file: %v", err) log.Fatalf("Failed to save the storage file: %s %v", fileName, err)
} }
} }