mirror of
https://github.com/status-im/status-go.git
synced 2025-01-14 00:36:40 +00:00
07e46714f0
* Protobufs and adapters * Added basic anon metric service and config init * Added fibonacci interval incrementer * Added basic Client.Start func and integrated interval incrementer * Added new processed field to app metrics table * Added id column to app metrics table * Added migration clean up * Added appmetrics GetUnprocessed and SetToProcessedByIDs and tests There was a wierd bug where metrics in the db that did not explicitly insert a value would be NULL, so could not be found by . In addition I've added a new primary id field to the app_metrics table so that updates could be done against very specific metric rows. * Updated adaptors and db to handle proto_id I need a way to distinguish individual metric items from each other so that I can ignore the ones that have been seen before. * Moved incrementer into dedicated file * Resolve incrementer test fail * Finalised the main loop functionality * Implemented delete loop framework * Updated adaptors file name * Added delete loop delay and quit, and tweak on RawMessage gen * Completed delete loop logic * Added DBLock to prevent deletion during mainLoop * Added postgres DB connection, integrated into anonmetrics.Server * Removed proto_id from SQL migration and model * Integrated postgres with Server and updated adaptors * Function name update * Added sample config files for client and server * Fixes and testing for low level e2e * make generate * Fix lint * Fix for receiving an anonMetricBatch not in server mode * Postgres test fixes * Tidy up, make vendor and make generate * delinting * Fixing database tests * Attempted fix of does: cannot open `does' (No such file or directory) not: cannot open `not' (No such file or directory) exist: cannot open `exist' (No such file or directory) error on sql resource loas * Moved all anon metric postgres migration logic and sources into a the protocol/anonmetrics package or sub packages. I don't know if this will fix the does: cannot open `does' (No such file or directory) not: cannot open `not' (No such file or directory) exist: cannot open `exist' (No such file or directory) error that happens in Jenkins but this could work * Lint for the lint god * Why doesn't the linter list all its problems at once? * test tweaks * Fix for wakuV2 change * DB reset change * Fix for postgres db migrations fails * More robust implementation of postgres test setup and teardown * Added block for anon metrics functionality * Version Bump to 0.84.0 * Added test to check anon metrics broadcast is deactivated * Protobufs and adapters * Added basic anon metric service and config init * Added new processed field to app metrics table * Added id column to app metrics table * Added migration clean up * Added appmetrics GetUnprocessed and SetToProcessedByIDs and tests There was a wierd bug where metrics in the db that did not explicitly insert a value would be NULL, so could not be found by . In addition I've added a new primary id field to the app_metrics table so that updates could be done against very specific metric rows. * Updated adaptors and db to handle proto_id I need a way to distinguish individual metric items from each other so that I can ignore the ones that have been seen before. * Added postgres DB connection, integrated into anonmetrics.Server * Removed proto_id from SQL migration and model * Integrated postgres with Server and updated adaptors * Added sample config files for client and server * Fix lint * Fix for receiving an anonMetricBatch not in server mode * Postgres test fixes * Tidy up, make vendor and make generate * Moved all anon metric postgres migration logic and sources into a the protocol/anonmetrics package or sub packages. I don't know if this will fix the does: cannot open `does' (No such file or directory) not: cannot open `not' (No such file or directory) exist: cannot open `exist' (No such file or directory) error that happens in Jenkins but this could work
316 lines
7.0 KiB
Go
316 lines
7.0 KiB
Go
package appmetrics
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/xeipuuv/gojsonschema"
|
|
)
|
|
|
|
type AppMetricEventType string
|
|
|
|
// Value is `json.RawMessage` so we can send any json shape, including strings
|
|
// Validation is handled using JSON schemas defined in validators.go, instead of Golang structs
|
|
type AppMetric struct {
|
|
ID int `json:"-"`
|
|
MessageID string `json:"message_id"`
|
|
Event AppMetricEventType `json:"event"`
|
|
Value json.RawMessage `json:"value"`
|
|
AppVersion string `json:"app_version"`
|
|
OS string `json:"os"`
|
|
SessionID string `json:"session_id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Processed bool `json:"processed"`
|
|
ReceivedAt time.Time `json:"received_at"`
|
|
}
|
|
|
|
type AppMetricValidationError struct {
|
|
Metric AppMetric
|
|
Errors []gojsonschema.ResultError
|
|
}
|
|
|
|
type Page struct {
|
|
AppMetrics []AppMetric
|
|
TotalCount int
|
|
}
|
|
|
|
const (
|
|
// status-react navigation events
|
|
NavigateTo AppMetricEventType = "navigate-to"
|
|
ScreensOnWillFocus AppMetricEventType = "screens/on-will-focus"
|
|
)
|
|
|
|
// EventSchemaMap Every event should have a schema attached
|
|
var EventSchemaMap = map[AppMetricEventType]interface{}{
|
|
NavigateTo: NavigateToCofxSchema,
|
|
ScreensOnWillFocus: NavigateToCofxSchema,
|
|
}
|
|
|
|
func NewDB(db *sql.DB) *Database {
|
|
return &Database{db: db}
|
|
}
|
|
|
|
// Database sql wrapper for operations with browser objects.
|
|
type Database struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// Close closes database.
|
|
func (db Database) Close() error {
|
|
return db.db.Close()
|
|
}
|
|
|
|
func jsonschemaErrorsToError(validationErrors []AppMetricValidationError) error {
|
|
var fieldErrors []string
|
|
|
|
for _, appMetricValidationError := range validationErrors {
|
|
metric := appMetricValidationError.Metric
|
|
errors := appMetricValidationError.Errors
|
|
|
|
var errorDesc string = "Error in event: " + string(metric.Event) + " - "
|
|
for _, e := range errors {
|
|
errorDesc = errorDesc + "value." + e.Context().String() + ":" + e.Description()
|
|
}
|
|
fieldErrors = append(fieldErrors, errorDesc)
|
|
}
|
|
|
|
return errors.New(strings.Join(fieldErrors[:], "/ "))
|
|
}
|
|
|
|
func (db *Database) ValidateAppMetrics(appMetrics []AppMetric) (err error) {
|
|
var calculatedErrors []AppMetricValidationError
|
|
for _, metric := range appMetrics {
|
|
schema := EventSchemaMap[metric.Event]
|
|
|
|
if schema == nil {
|
|
return errors.New("No schema defined for: " + string(metric.Event))
|
|
}
|
|
|
|
schemaLoader := gojsonschema.NewGoLoader(schema)
|
|
valLoader := gojsonschema.NewStringLoader(string(metric.Value))
|
|
res, err := gojsonschema.Validate(schemaLoader, valLoader)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// validate all metrics and save errors
|
|
if !res.Valid() {
|
|
calculatedErrors = append(calculatedErrors, AppMetricValidationError{metric, res.Errors()})
|
|
}
|
|
}
|
|
|
|
if len(calculatedErrors) > 0 {
|
|
return jsonschemaErrorsToError(calculatedErrors)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (db *Database) SaveAppMetrics(appMetrics []AppMetric, sessionID string) (err error) {
|
|
var (
|
|
tx *sql.Tx
|
|
insert *sql.Stmt
|
|
)
|
|
|
|
// make sure that the shape of the metric is same as expected
|
|
err = db.ValidateAppMetrics(appMetrics)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// start txn
|
|
tx, err = db.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err == nil {
|
|
err = tx.Commit()
|
|
return
|
|
}
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
insert, err = tx.Prepare("INSERT INTO app_metrics (event, value, app_version, operating_system, session_id, processed) VALUES (?, ?, ?, ?, ?, ?)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, metric := range appMetrics {
|
|
_, err = insert.Exec(metric.Event, metric.Value, metric.AppVersion, metric.OS, sessionID, metric.Processed)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (db *Database) GetAppMetrics(limit int, offset int) (page Page, err error) {
|
|
countErr := db.db.QueryRow("SELECT count(*) FROM app_metrics").Scan(&page.TotalCount)
|
|
if countErr != nil {
|
|
return page, countErr
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return page, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
page.AppMetrics, err = db.getFromRows(rows)
|
|
return page, err
|
|
}
|
|
|
|
func (db *Database) getFromRows(rows *sql.Rows) (appMetrics []AppMetric, err error) {
|
|
var metrics []AppMetric
|
|
|
|
for rows.Next() {
|
|
metric := AppMetric{}
|
|
err = rows.Scan(
|
|
&metric.ID,
|
|
&metric.Event,
|
|
&metric.Value,
|
|
&metric.AppVersion,
|
|
&metric.OS,
|
|
&metric.SessionID,
|
|
&metric.CreatedAt,
|
|
&metric.Processed,
|
|
)
|
|
if err != nil {
|
|
return metrics, err
|
|
}
|
|
metrics = append(metrics, metric)
|
|
}
|
|
return metrics, nil
|
|
}
|
|
|
|
func (db *Database) GetUnprocessed() ([]AppMetric, error) {
|
|
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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
return db.getFromRows(rows)
|
|
}
|
|
|
|
func (db *Database) GetUnprocessedGroupedBySession() (map[string][]AppMetric, error) {
|
|
uam, err := db.GetUnprocessed()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
out := map[string][]AppMetric{}
|
|
for _, am := range uam {
|
|
out[am.SessionID] = append(out[am.SessionID], am)
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
func (db *Database) SetToProcessedByIDs(ids []int) (err error) {
|
|
var (
|
|
tx *sql.Tx
|
|
update *sql.Stmt
|
|
)
|
|
|
|
// start txn
|
|
tx, err = db.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err == nil {
|
|
err = tx.Commit()
|
|
return
|
|
}
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
// Generate prepared statement IN list
|
|
in := "("
|
|
for i := 0; i < len(ids); i++ {
|
|
in += "?,"
|
|
}
|
|
in = in[:len(in)-1] + ")"
|
|
|
|
update, err = tx.Prepare("UPDATE app_metrics SET processed = 1 WHERE id IN " + in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Convert the ids into Stmt.Exec compatible variadic
|
|
args := make([]interface{}, 0, len(ids))
|
|
for _, id := range ids {
|
|
args = append(args, id)
|
|
}
|
|
|
|
_, err = update.Exec(args...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func (db *Database) SetToProcessed(appMetrics []AppMetric) (err error) {
|
|
ids := GetAppMetricsIDs(appMetrics)
|
|
return db.SetToProcessedByIDs(ids)
|
|
}
|
|
|
|
func (db *Database) GetMessagesOlderThan(date *time.Time) ([]AppMetric, error) {
|
|
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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
return db.getFromRows(rows)
|
|
}
|
|
|
|
func (db *Database) DeleteOlderThan(date *time.Time) (err error) {
|
|
var (
|
|
tx *sql.Tx
|
|
d *sql.Stmt
|
|
)
|
|
|
|
// start txn
|
|
tx, err = db.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() {
|
|
if err == nil {
|
|
err = tx.Commit()
|
|
return
|
|
}
|
|
_ = tx.Rollback()
|
|
}()
|
|
|
|
d, err = tx.Prepare("DELETE FROM app_metrics WHERE created_at < ?")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = d.Exec(date)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func GetAppMetricsIDs(appMetrics []AppMetric) []int {
|
|
var ids []int
|
|
|
|
for _, am := range appMetrics {
|
|
ids = append(ids, am.ID)
|
|
}
|
|
|
|
return ids
|
|
}
|