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  }