added telemetry reports and license registration
This commit is contained in:
parent
eb2f9949b6
commit
f5fdf635f0
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
107
main.go
107
main.go
|
@ -21,10 +21,38 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Starter struct {
|
type Starter struct {
|
||||||
lout *layout.Layout
|
player *asset.AudioPlayer
|
||||||
player *asset.AudioPlayer
|
lout *layout.Layout
|
||||||
opt config.Options
|
palette console.Palette
|
||||||
cfg config.Config
|
opt config.Options
|
||||||
|
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) {
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue