github.com/status-im/status-go@v1.1.0/centralizedmetrics/providers/mixpanel.go (about) 1 package providers 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net/http" 10 11 "github.com/ethereum/go-ethereum/log" 12 13 "github.com/status-im/status-go/centralizedmetrics/common" 14 ) 15 16 const MixpanelBaseURL = "https://api.mixpanel.com" 17 18 var MixpanelToken = "" 19 var MixpanelAppID = "" 20 21 // MixpanelMetricProcessor implements MetricProcessor for Mixpanel 22 type MixpanelMetricProcessor struct { 23 appID string 24 secret string 25 baseURL string 26 } 27 28 // NewMixpanelMetricProcessor is a constructor for MixpanelMetricProcessor 29 func NewMixpanelMetricProcessor(appID, secret, baseURL string) *MixpanelMetricProcessor { 30 return &MixpanelMetricProcessor{ 31 appID: appID, 32 secret: secret, 33 baseURL: baseURL, 34 } 35 } 36 37 func (amp *MixpanelMetricProcessor) GetAppID() string { 38 if len(amp.appID) != 0 { 39 return amp.appID 40 } 41 return MixpanelAppID 42 } 43 44 func (amp *MixpanelMetricProcessor) GetToken() string { 45 if len(amp.secret) != 0 { 46 return amp.secret 47 } 48 49 return MixpanelToken 50 } 51 52 // Process processes an array of metrics and sends them to the Mixpanel API 53 func (amp *MixpanelMetricProcessor) Process(metrics []common.Metric) error { 54 if err := amp.sendToMixpanel(metrics); err != nil { 55 return err 56 } 57 return nil 58 } 59 60 // sendToMixpanel sends a single metric to the Mixpanel API 61 func (amp *MixpanelMetricProcessor) sendToMixpanel(metrics []common.Metric) error { 62 url := fmt.Sprintf("%s/track?project_id=%s&strict=1", amp.baseURL, amp.GetAppID()) 63 64 var mixPanelMetrics []mixpanelMetric 65 66 for _, metric := range metrics { 67 mixPanelMetrics = append(mixPanelMetrics, toMixpanelMetric(metric, amp.GetToken())) 68 } 69 payload, err := json.Marshal(mixPanelMetrics) 70 if err != nil { 71 return err 72 } 73 74 log.Info("sending metrics to", "url", url, "metric", mixPanelMetrics, "secret", amp.GetToken()) 75 76 req, err := http.NewRequest("POST", url, bytes.NewBuffer(payload)) 77 if err != nil { 78 return err 79 } 80 81 req.Header.Set("accept", "application/json") 82 req.Header.Set("content-type", "application/json") 83 84 client := &http.Client{} 85 resp, err := client.Do(req) 86 if err != nil { 87 return err 88 } 89 defer resp.Body.Close() 90 91 if resp.StatusCode != http.StatusOK { 92 body, err := io.ReadAll(resp.Body) 93 fmt.Println(resp.StatusCode, string(body), err) 94 log.Warn("failed to send metric", "status-code", resp.StatusCode, "body", resp.Body) 95 return errors.New("failed to send metric to Mixpanel") 96 } 97 98 return nil 99 } 100 101 func toMixpanelMetric(metric common.Metric, token string) mixpanelMetric { 102 103 properties := mixpanelMetricProperties{ 104 Time: metric.Timestamp, 105 UserID: metric.UserID, 106 Platform: metric.Platform, 107 InsertID: metric.ID, 108 AppVersion: metric.AppVersion, 109 Token: token, 110 AdditionalProperties: metric.EventValue, 111 } 112 113 return mixpanelMetric{ 114 Event: metric.EventName, 115 Properties: properties, 116 } 117 } 118 119 type mixpanelMetricProperties struct { 120 Time int64 `json:"time"` 121 UserID string `json:"distinct_id"` 122 InsertID string `json:"$insert_id"` 123 Platform string `json:"platform"` 124 AppVersion string `json:"app_version"` 125 AdditionalProperties map[string]any `json:"-"` 126 Token string `json:"token"` 127 } 128 129 type mixpanelMetric struct { 130 Event string `json:"event"` 131 Properties mixpanelMetricProperties `json:"properties"` 132 } 133 134 func (p mixpanelMetricProperties) MarshalJSON() ([]byte, error) { 135 // Create a map and marshal the struct fields into it 136 type alias mixpanelMetricProperties // Alias to avoid recursion 137 data, err := json.Marshal(alias(p)) 138 if err != nil { 139 return nil, err 140 } 141 142 // Unmarshal the JSON into a map 143 var mmpMap map[string]any 144 if err := json.Unmarshal(data, &mmpMap); err != nil { 145 return nil, err 146 } 147 148 // Merge AdditionalProperties into the map 149 for key, value := range p.AdditionalProperties { 150 mmpMap[key] = value 151 } 152 153 // Marshal the merged map back to JSON 154 marshaled, err := json.Marshal(mmpMap) 155 156 return marshaled, err 157 } 158 159 func (p *mixpanelMetricProperties) UnmarshalJSON(data []byte) error { 160 // Create a temporary alias type to unmarshal known fields 161 type alias mixpanelMetricProperties 162 aux := &struct { 163 *alias 164 }{ 165 alias: (*alias)(p), 166 } 167 168 if err := json.Unmarshal(data, &aux); err != nil { 169 return err 170 } 171 172 // Unmarshal into a map to capture additional properties 173 var rawMap map[string]any 174 if err := json.Unmarshal(data, &rawMap); err != nil { 175 return err 176 } 177 178 // Remove known fields from the map 179 delete(rawMap, "time") 180 delete(rawMap, "token") 181 delete(rawMap, "distinct_id") 182 delete(rawMap, "$insert_id") 183 delete(rawMap, "platform") 184 delete(rawMap, "app_version") 185 186 // Assign the remaining fields to AdditionalProperties 187 p.AdditionalProperties = rawMap 188 189 return nil 190 }