Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/supervisor/pkg/config/gitpod-config-analytics.go
2500 views
1
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package config
6
7
import (
8
"crypto/sha256"
9
"encoding/json"
10
"fmt"
11
"reflect"
12
"sync"
13
"time"
14
15
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
16
"github.com/sirupsen/logrus"
17
)
18
19
var emptyConfig = &gitpod.GitpodConfig{}
20
21
type ConfigAnalyzer struct {
22
log *logrus.Entry
23
report func(field string)
24
delay time.Duration
25
prev *gitpod.GitpodConfig
26
timer *time.Timer
27
mu sync.RWMutex
28
}
29
30
func NewConfigAnalyzer(
31
log *logrus.Entry,
32
delay time.Duration,
33
report func(field string),
34
initial *gitpod.GitpodConfig,
35
) *ConfigAnalyzer {
36
prev := emptyConfig
37
if initial != nil {
38
prev = initial
39
}
40
log.WithField("initial", prev).Debug("gitpod config analytics: initialized")
41
return &ConfigAnalyzer{
42
log: log,
43
delay: delay,
44
report: report,
45
prev: prev,
46
}
47
}
48
49
func (a *ConfigAnalyzer) Analyse(cfg *gitpod.GitpodConfig) <-chan struct{} {
50
current := emptyConfig
51
if cfg != nil {
52
current = cfg
53
}
54
55
a.log.Debug("gitpod config analytics: scheduling")
56
a.mu.Lock()
57
defer a.mu.Unlock()
58
59
if a.timer != nil && !a.timer.Stop() {
60
a.log.WithField("cfg", current).Debug("gitpod config analytics: cancelled")
61
<-a.timer.C
62
}
63
64
done := make(chan struct{})
65
a.timer = time.AfterFunc(a.delay, func() {
66
a.mu.Lock()
67
defer a.mu.Unlock()
68
a.process(current)
69
a.timer = nil
70
close(done)
71
})
72
return done
73
}
74
75
func (a *ConfigAnalyzer) process(current *gitpod.GitpodConfig) {
76
a.log.Debug("gitpod config analytics: processing")
77
78
fields := a.computeFields(a.prev, current)
79
for _, field := range fields {
80
if a.diffByField(a.prev, current, field) {
81
a.report(field)
82
}
83
}
84
85
a.log.WithField("current", current).
86
WithField("prev", a.prev).
87
WithField("fields", fields).
88
Debug("gitpod config analytics: processed")
89
90
a.prev = current
91
}
92
93
func (a *ConfigAnalyzer) computeFields(configs ...*gitpod.GitpodConfig) []string {
94
defer func() {
95
if err := recover(); err != nil {
96
a.log.WithField("error", err).Error("gitpod config analytics: failed to compute gitpod config fields")
97
}
98
}()
99
var fields []string
100
uniqueKeys := make(map[string]struct{})
101
for _, cfg := range configs {
102
if cfg == nil {
103
continue
104
}
105
cfgType := reflect.ValueOf(*cfg).Type()
106
if cfgType.Kind() == reflect.Struct {
107
for i := 0; i < cfgType.NumField(); i++ {
108
Name := cfgType.Field(i).Name
109
_, seen := uniqueKeys[Name]
110
if !seen {
111
uniqueKeys[Name] = struct{}{}
112
fields = append(fields, Name)
113
}
114
}
115
}
116
}
117
return fields
118
}
119
120
func (a *ConfigAnalyzer) valueByField(config *gitpod.GitpodConfig, field string) interface{} {
121
defer func() {
122
if err := recover(); err != nil {
123
a.log.WithField("error", err).WithField("field", field).Error("gitpod config analytics: failed to retrieve value from gitpod config")
124
}
125
}()
126
if config == nil {
127
return nil
128
}
129
return reflect.ValueOf(*config).FieldByName(field).Interface()
130
}
131
132
func (a *ConfigAnalyzer) computeHash(i interface{}) (string, error) {
133
b, err := json.Marshal(i)
134
if err != nil {
135
return "", err
136
}
137
h := sha256.New()
138
_, err = h.Write(b)
139
if err != nil {
140
return "", err
141
}
142
return fmt.Sprintf("%x", h.Sum(nil)), nil
143
}
144
145
func (a *ConfigAnalyzer) diffByField(prev *gitpod.GitpodConfig, current *gitpod.GitpodConfig, field string) bool {
146
defer func() {
147
if err := recover(); err != nil {
148
a.log.WithField("error", err).WithField("field", field).Error("gitpod config analytics: failed to compare gitpod configs")
149
}
150
}()
151
prevValue := a.valueByField(prev, field)
152
prevHash, _ := a.computeHash(prevValue)
153
currentValue := a.valueByField(current, field)
154
currHash, _ := a.computeHash(currentValue)
155
return prevHash != currHash
156
}
157
158