Skip to content

Commit d2d944b

Browse files
committed
feat(usage): make measurement ID and api secret configurable via envs
Signed-off-by: Niladri Halder <[email protected]>
1 parent 17f9b1f commit d2d944b

File tree

4 files changed

+182
-25
lines changed

4 files changed

+182
-25
lines changed

pkg/client/build.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import (
2525
"github.com/openebs/lib-csi/pkg/common/errors"
2626
)
2727

28-
var measurementIDMatcher = regexp.MustCompile(`^G-[a-zA-Z0-9]+$`)
28+
var MeasurementIDMatcher = regexp.MustCompile(`^G-[a-zA-Z0-9]+$`)
2929

3030
type MeasurementClientOption func(*MeasurementClient) error
3131

@@ -82,7 +82,7 @@ func WithMeasurementId(measurementId string) MeasurementClientOption {
8282
return errors.Errorf("failed to set measurement_id: id is an empty string")
8383
}
8484

85-
if !measurementIDMatcher.MatchString(measurementId) {
85+
if !MeasurementIDMatcher.MatchString(measurementId) {
8686
return errors.Errorf("Invalid measurement_id: %s", measurementId)
8787
}
8888

usage/const.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package usage
22

33
const (
4-
// MeasurementId is the unique code of the OpenEBS property in Google Analytics.
5-
MeasurementId string = "G-TZGP46618W"
6-
// ApiSecret is the measurement protocol api_secret.
7-
ApiSecret string = "91JGdTg9QwGn7Y-vvuM4zA"
4+
// DefaultMeasurementId is the default unique code of the OpenEBS property in Google Analytics.
5+
DefaultMeasurementId string = "G-TZGP46618W"
6+
// DefaultApiSecret is the default measurement protocol api_secret.
7+
DefaultApiSecret string = "91JGdTg9QwGn7Y-vvuM4zA"
88

99
// InstallEvent event is sent on pod starts
1010
InstallEvent string = "install"
@@ -26,4 +26,9 @@ const (
2626
EventLabelNode string = "nodes"
2727
// EventLabelCapacity holds the string label "capacity"
2828
EventLabelCapacity string = "capacity"
29+
30+
// MeasurementIdEnv sets the measurement ID for the target GA4 property.
31+
MeasurementIdEnv = "GA_ID"
32+
// ApiSecretEnv sets the measurement protocol API secret for the target GA4 property.
33+
ApiSecretEnv = "GA_KEY"
2934
)

usage/usage.go

+39-19
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,49 @@
1-
/*
2-
Copyright 2023 The OpenEBS Authors.
3-
4-
Licensed under the Apache License, Version 2.0 (the "License");
5-
you may not use this file except in compliance with the License.
6-
You may obtain a copy of the License at
7-
8-
http://www.apache.org/licenses/LICENSE-2.0
9-
10-
Unless required by applicable law or agreed to in writing, software
11-
distributed under the License is distributed on an "AS IS" BASIS,
12-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
See the License for the specific language governing permissions and
14-
limitations under the License.
15-
*/
16-
171
package usage
182

193
import (
4+
"encoding/base64"
5+
"os"
206
"strconv"
217

8+
k8sapi "github.com/openebs/lib-csi/pkg/client/k8s"
229
"k8s.io/klog/v2"
2310

2411
ga4Client "github.com/openebs/google-analytics-4/pkg/client"
2512
ga4Event "github.com/openebs/google-analytics-4/pkg/event"
26-
k8sapi "github.com/openebs/lib-csi/pkg/client/k8s"
2713
)
2814

15+
// apiCreds reads envs and decodes base64 input for GA-4 MeasurementId and ApiSecret.
16+
// Returns defaults if envs are unset/invalid.
17+
func apiCreds() (string, string) {
18+
encodedId, idExists := os.LookupEnv(MeasurementIdEnv)
19+
encodedSecret, secretExists := os.LookupEnv(ApiSecretEnv)
20+
21+
// Use defaults if envs are unset or they are set and value(s) are empty.
22+
if !idExists || !secretExists || (len(encodedId) == 0) || (len(encodedSecret) == 0) {
23+
return DefaultMeasurementId, DefaultApiSecret
24+
}
25+
26+
// Use defaults if the envs are not valid base64 strings.
27+
id, errId := base64.StdEncoding.DecodeString(encodedId)
28+
if errId != nil {
29+
klog.Errorf("Failed to decode measurement id: %s", errId.Error())
30+
return DefaultMeasurementId, DefaultApiSecret
31+
}
32+
secret, errSecret := base64.StdEncoding.DecodeString(encodedSecret)
33+
if errSecret != nil {
34+
klog.Errorf("Failed to decode secret: %s", errSecret.Error())
35+
return DefaultMeasurementId, DefaultApiSecret
36+
}
37+
38+
// Use defaults if the input measurement ID doesn't match the regex.
39+
if !ga4Client.MeasurementIDMatcher.Match(id) {
40+
klog.Errorf("Measurement ID does not match regex")
41+
return DefaultMeasurementId, DefaultApiSecret
42+
}
43+
44+
return string(id), string(secret)
45+
}
46+
2947
// Usage struct represents all information about a usage metric sent to
3048
// Google Analytics with respect to the application
3149
type Usage struct {
@@ -38,9 +56,11 @@ type Usage struct {
3856

3957
// New returns an instance of Usage
4058
func New() *Usage {
59+
measurementId, apiSecret := apiCreds()
60+
4161
client, err := ga4Client.NewMeasurementClient(
42-
ga4Client.WithApiSecret(ApiSecret),
43-
ga4Client.WithMeasurementId(MeasurementId),
62+
ga4Client.WithApiSecret(apiSecret),
63+
ga4Client.WithMeasurementId(measurementId),
4464
)
4565
if err != nil {
4666
return nil

usage/usage_test.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package usage
2+
3+
import (
4+
"encoding/base64"
5+
"os"
6+
"reflect"
7+
"testing"
8+
"unicode/utf8"
9+
)
10+
11+
func TestApiCreds(t *testing.T) {
12+
testCases := map[string]struct {
13+
idEnvValue string
14+
idSet bool
15+
secretEnvValue string
16+
secretSet bool
17+
expectedId string
18+
expectedSecret string
19+
}{
20+
"Missing both ENVs": {
21+
idEnvValue: "",
22+
idSet: false,
23+
secretEnvValue: "",
24+
secretSet: false,
25+
expectedId: DefaultMeasurementId,
26+
expectedSecret: DefaultApiSecret,
27+
},
28+
"Missing id ENV": {
29+
idEnvValue: "",
30+
idSet: false,
31+
secretEnvValue: base64.StdEncoding.EncodeToString([]byte("testSecret")),
32+
secretSet: true,
33+
expectedId: DefaultMeasurementId,
34+
expectedSecret: DefaultApiSecret,
35+
},
36+
"Missing secret ENV": {
37+
idEnvValue: base64.StdEncoding.EncodeToString([]byte("G-testId")),
38+
idSet: true,
39+
secretEnvValue: "",
40+
secretSet: false,
41+
expectedId: DefaultMeasurementId,
42+
expectedSecret: DefaultApiSecret,
43+
},
44+
"Empty id ENV": {
45+
idEnvValue: "",
46+
idSet: true,
47+
secretEnvValue: base64.StdEncoding.EncodeToString([]byte("testSecret")),
48+
secretSet: true,
49+
expectedId: DefaultMeasurementId,
50+
expectedSecret: DefaultApiSecret,
51+
},
52+
"Empty secret ENV": {
53+
idEnvValue: base64.StdEncoding.EncodeToString([]byte("G-testId")),
54+
idSet: true,
55+
secretEnvValue: "",
56+
secretSet: true,
57+
expectedId: DefaultMeasurementId,
58+
expectedSecret: DefaultApiSecret,
59+
},
60+
"Invalid base64 value for id ENV": {
61+
idEnvValue: trimLastChar(base64.StdEncoding.EncodeToString([]byte("G-testId"))),
62+
idSet: true,
63+
secretEnvValue: base64.StdEncoding.EncodeToString([]byte("testSecret")),
64+
secretSet: true,
65+
expectedId: DefaultMeasurementId,
66+
expectedSecret: DefaultApiSecret,
67+
},
68+
"Invalid base64 value for secret ENV": {
69+
idEnvValue: base64.StdEncoding.EncodeToString([]byte("testId")),
70+
idSet: true,
71+
secretEnvValue: trimLastChar(base64.StdEncoding.EncodeToString([]byte("testSecret"))),
72+
secretSet: true,
73+
expectedId: DefaultMeasurementId,
74+
expectedSecret: DefaultApiSecret,
75+
},
76+
"Valid ENVs set": {
77+
idEnvValue: base64.StdEncoding.EncodeToString([]byte("G-testId")),
78+
idSet: true,
79+
secretEnvValue: base64.StdEncoding.EncodeToString([]byte("testSecret")),
80+
secretSet: true,
81+
expectedId: "G-testId",
82+
expectedSecret: "testSecret",
83+
},
84+
}
85+
86+
for k, v := range testCases {
87+
k, v := k, v
88+
t.Run(k, func(t *testing.T) {
89+
if v.idSet {
90+
if os.Setenv(MeasurementIdEnv, v.idEnvValue) != nil {
91+
t.Errorf("failed to set env '%s' to '%s' for test case '%s'",
92+
MeasurementIdEnv, v.idEnvValue, k,
93+
)
94+
}
95+
} else {
96+
if os.Unsetenv(MeasurementIdEnv) != nil {
97+
t.Errorf("failed to unset env '%s' for test case '%s'", MeasurementIdEnv, k)
98+
}
99+
}
100+
if v.secretSet {
101+
if os.Setenv(ApiSecretEnv, v.secretEnvValue) != nil {
102+
t.Errorf("failed to set env '%s' to '%s' for test case '%s'",
103+
ApiSecretEnv, v.secretEnvValue, k,
104+
)
105+
}
106+
} else {
107+
if os.Unsetenv(ApiSecretEnv) != nil {
108+
t.Errorf("failed to unset env '%s' for test case '%s'", ApiSecretEnv, k)
109+
}
110+
}
111+
observedId, observedSecret := apiCreds()
112+
if !reflect.DeepEqual(observedId, v.expectedId) {
113+
t.Errorf("apiCreds() id mismatch: expected '%s', observed '%s'",
114+
v.expectedId, observedId,
115+
)
116+
}
117+
if !reflect.DeepEqual(observedSecret, v.expectedSecret) {
118+
t.Errorf("apiCreds() secret mismatch: expected '%s', observed '%s'",
119+
v.expectedSecret, observedSecret,
120+
)
121+
}
122+
})
123+
}
124+
}
125+
126+
func trimLastChar(s string) string {
127+
r, size := utf8.DecodeLastRuneInString(s)
128+
if r == utf8.RuneError && (size == 0 || size == 1) {
129+
size = 0
130+
}
131+
return s[:len(s)-size]
132+
}

0 commit comments

Comments
 (0)