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 }