github.com/status-im/status-go@v1.1.0/appmetrics/database.go (about)

     1  package appmetrics
     2  
     3  import (
     4  	"database/sql"
     5  	"encoding/json"
     6  	"errors"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/xeipuuv/gojsonschema"
    11  )
    12  
    13  type AppMetricEventType string
    14  
    15  // Value is `json.RawMessage` so we can send any json shape, including strings
    16  // Validation is handled using JSON schemas defined in validators.go, instead of Golang structs
    17  type AppMetric struct {
    18  	ID         int                `json:"-"`
    19  	MessageID  string             `json:"message_id"`
    20  	Event      AppMetricEventType `json:"event"`
    21  	Value      json.RawMessage    `json:"value"`
    22  	AppVersion string             `json:"app_version"`
    23  	OS         string             `json:"os"`
    24  	SessionID  string             `json:"session_id"`
    25  	CreatedAt  time.Time          `json:"created_at"`
    26  	Processed  bool               `json:"processed"`
    27  	ReceivedAt time.Time          `json:"received_at"`
    28  }
    29  
    30  type AppMetricValidationError struct {
    31  	Metric AppMetric
    32  	Errors []gojsonschema.ResultError
    33  }
    34  
    35  type Page struct {
    36  	AppMetrics []AppMetric
    37  	TotalCount int
    38  }
    39  
    40  const (
    41  	// status-mobile navigation events
    42  	NavigateTo         AppMetricEventType = "navigate-to"
    43  	ScreensOnWillFocus AppMetricEventType = "screens/on-will-focus"
    44  )
    45  
    46  // EventSchemaMap Every event should have a schema attached
    47  var EventSchemaMap = map[AppMetricEventType]interface{}{
    48  	NavigateTo:         NavigateToCofxSchema,
    49  	ScreensOnWillFocus: NavigateToCofxSchema,
    50  }
    51  
    52  func NewDB(db *sql.DB) *Database {
    53  	return &Database{db: db}
    54  }
    55  
    56  // Database sql wrapper for operations with browser objects.
    57  type Database struct {
    58  	db *sql.DB
    59  }
    60  
    61  // Close closes database.
    62  func (db Database) Close() error {
    63  	return db.db.Close()
    64  }
    65  
    66  func jsonschemaErrorsToError(validationErrors []AppMetricValidationError) error {
    67  	var fieldErrors []string
    68  
    69  	for _, appMetricValidationError := range validationErrors {
    70  		metric := appMetricValidationError.Metric
    71  		errors := appMetricValidationError.Errors
    72  
    73  		var errorDesc string = "Error in event: " + string(metric.Event) + " - "
    74  		for _, e := range errors {
    75  			errorDesc = errorDesc + "value." + e.Context().String() + ":" + e.Description()
    76  		}
    77  		fieldErrors = append(fieldErrors, errorDesc)
    78  	}
    79  
    80  	return errors.New(strings.Join(fieldErrors[:], "/ "))
    81  }
    82  
    83  func (db *Database) ValidateAppMetrics(appMetrics []AppMetric) (err error) {
    84  	var calculatedErrors []AppMetricValidationError
    85  	for _, metric := range appMetrics {
    86  		schema := EventSchemaMap[metric.Event]
    87  
    88  		if schema == nil {
    89  			return errors.New("No schema defined for: " + string(metric.Event))
    90  		}
    91  
    92  		schemaLoader := gojsonschema.NewGoLoader(schema)
    93  		valLoader := gojsonschema.NewStringLoader(string(metric.Value))
    94  		res, err := gojsonschema.Validate(schemaLoader, valLoader)
    95  
    96  		if err != nil {
    97  			return err
    98  		}
    99  
   100  		// validate all metrics and save errors
   101  		if !res.Valid() {
   102  			calculatedErrors = append(calculatedErrors, AppMetricValidationError{metric, res.Errors()})
   103  		}
   104  	}
   105  
   106  	if len(calculatedErrors) > 0 {
   107  		return jsonschemaErrorsToError(calculatedErrors)
   108  	}
   109  	return
   110  }
   111  
   112  func (db *Database) SaveAppMetrics(appMetrics []AppMetric, sessionID string) (err error) {
   113  	var (
   114  		tx     *sql.Tx
   115  		insert *sql.Stmt
   116  	)
   117  
   118  	// make sure that the shape of the metric is same as expected
   119  	err = db.ValidateAppMetrics(appMetrics)
   120  	if err != nil {
   121  		return err
   122  	}
   123  
   124  	// start txn
   125  	tx, err = db.db.Begin()
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	defer func() {
   131  		if err == nil {
   132  			err = tx.Commit()
   133  			return
   134  		}
   135  		_ = tx.Rollback()
   136  	}()
   137  
   138  	insert, err = tx.Prepare("INSERT INTO app_metrics (event, value, app_version, operating_system, session_id, processed) VALUES (?, ?, ?, ?, ?, ?)")
   139  	if err != nil {
   140  		return err
   141  	}
   142  
   143  	for _, metric := range appMetrics {
   144  		_, err = insert.Exec(metric.Event, metric.Value, metric.AppVersion, metric.OS, sessionID, metric.Processed)
   145  		if err != nil {
   146  			return
   147  		}
   148  	}
   149  	return
   150  }
   151  
   152  func (db *Database) GetAppMetrics(limit int, offset int) (page Page, err error) {
   153  	countErr := db.db.QueryRow("SELECT count(*) FROM app_metrics").Scan(&page.TotalCount)
   154  	if countErr != nil {
   155  		return page, countErr
   156  	}
   157  
   158  	rows, err := db.db.Query("SELECT id, event, value, app_version, operating_system, session_id, created_at, processed FROM app_metrics LIMIT ? OFFSET ?", limit, offset)
   159  	if err != nil {
   160  		return page, err
   161  	}
   162  	defer rows.Close()
   163  
   164  	page.AppMetrics, err = db.getFromRows(rows)
   165  	return page, err
   166  }
   167  
   168  func (db *Database) getFromRows(rows *sql.Rows) (appMetrics []AppMetric, err error) {
   169  	var metrics []AppMetric
   170  
   171  	for rows.Next() {
   172  		metric := AppMetric{}
   173  		err = rows.Scan(
   174  			&metric.ID,
   175  			&metric.Event,
   176  			&metric.Value,
   177  			&metric.AppVersion,
   178  			&metric.OS,
   179  			&metric.SessionID,
   180  			&metric.CreatedAt,
   181  			&metric.Processed,
   182  		)
   183  		if err != nil {
   184  			return metrics, err
   185  		}
   186  		metrics = append(metrics, metric)
   187  	}
   188  	return metrics, nil
   189  }
   190  
   191  func (db *Database) GetUnprocessed() ([]AppMetric, error) {
   192  	rows, err := db.db.Query("SELECT id, event, value, app_version, operating_system, session_id, created_at, processed FROM app_metrics WHERE processed IS ? ORDER BY session_id ASC, created_at ASC", false)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	defer rows.Close()
   197  
   198  	return db.getFromRows(rows)
   199  }
   200  
   201  func (db *Database) GetUnprocessedGroupedBySession() (map[string][]AppMetric, error) {
   202  	uam, err := db.GetUnprocessed()
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	out := map[string][]AppMetric{}
   208  	for _, am := range uam {
   209  		out[am.SessionID] = append(out[am.SessionID], am)
   210  	}
   211  
   212  	return out, nil
   213  }
   214  
   215  func (db *Database) SetToProcessedByIDs(ids []int) (err error) {
   216  	var (
   217  		tx     *sql.Tx
   218  		update *sql.Stmt
   219  	)
   220  
   221  	// start txn
   222  	tx, err = db.db.Begin()
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	defer func() {
   228  		if err == nil {
   229  			err = tx.Commit()
   230  			return
   231  		}
   232  		_ = tx.Rollback()
   233  	}()
   234  
   235  	// Generate prepared statement IN list
   236  	in := "("
   237  	for i := 0; i < len(ids); i++ {
   238  		in += "?,"
   239  	}
   240  	in = in[:len(in)-1] + ")"
   241  
   242  	update, err = tx.Prepare("UPDATE app_metrics SET processed = 1 WHERE id IN " + in) // nolint: gosec
   243  	if err != nil {
   244  		return err
   245  	}
   246  
   247  	// Convert the ids into Stmt.Exec compatible variadic
   248  	args := make([]interface{}, 0, len(ids))
   249  	for _, id := range ids {
   250  		args = append(args, id)
   251  	}
   252  
   253  	_, err = update.Exec(args...)
   254  	if err != nil {
   255  		return
   256  	}
   257  	return
   258  }
   259  
   260  func (db *Database) SetToProcessed(appMetrics []AppMetric) (err error) {
   261  	ids := GetAppMetricsIDs(appMetrics)
   262  	return db.SetToProcessedByIDs(ids)
   263  }
   264  
   265  func (db *Database) GetMessagesOlderThan(date *time.Time) ([]AppMetric, error) {
   266  	rows, err := db.db.Query("SELECT id, event, value, app_version, operating_system, session_id, created_at, processed FROM app_metrics WHERE created_at < ?", date)
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	defer rows.Close()
   271  
   272  	return db.getFromRows(rows)
   273  }
   274  
   275  func (db *Database) DeleteOlderThan(date *time.Time) (err error) {
   276  	var (
   277  		tx *sql.Tx
   278  		d  *sql.Stmt
   279  	)
   280  
   281  	// start txn
   282  	tx, err = db.db.Begin()
   283  	if err != nil {
   284  		return err
   285  	}
   286  
   287  	defer func() {
   288  		if err == nil {
   289  			err = tx.Commit()
   290  			return
   291  		}
   292  		_ = tx.Rollback()
   293  	}()
   294  
   295  	d, err = tx.Prepare("DELETE FROM app_metrics WHERE created_at < ?")
   296  	if err != nil {
   297  		return err
   298  	}
   299  
   300  	_, err = d.Exec(date)
   301  	if err != nil {
   302  		return
   303  	}
   304  	return
   305  }
   306  
   307  func GetAppMetricsIDs(appMetrics []AppMetric) []int {
   308  	var ids []int
   309  
   310  	for _, am := range appMetrics {
   311  		ids = append(ids, am.ID)
   312  	}
   313  
   314  	return ids
   315  }