Files

321 lines
9.2 KiB
Go
Raw Permalink Normal View History

// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
package telemetry
import (
"context"
"database/sql/driver"
2024-04-05 22:42:22 -07:00
"fmt"
"log"
2024-04-05 22:42:22 -07:00
"regexp"
"strconv"
"time"
"github.com/wavetermdev/waveterm/wavesrv/pkg/dbutil"
"github.com/wavetermdev/waveterm/wavesrv/pkg/scbase"
2024-04-23 17:40:14 -07:00
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
)
const MaxTzNameLen = 50
2024-04-23 17:40:14 -07:00
// "terminal" should not be in this list
var allowedRenderers = map[string]bool{
"markdown": true,
"code": true,
"openai": true,
"csv": true,
"image": true,
"pdf": true,
"media": true,
"mustache": true,
}
type ActivityUpdate struct {
FgMinutes int
ActiveMinutes int
OpenMinutes int
NumCommands int
ClickShared int
HistoryView int
BookmarksView int
NumConns int
2024-04-23 17:40:14 -07:00
NumWorkspaces int
NumTabs int
NewTab int
ReinitBashErrors int
ReinitZshErrors int
2024-04-23 17:40:14 -07:00
Startup int
Shutdown int
FeAIChatOpen int
FeHistoryOpen int
2024-05-29 11:53:16 +08:00
FeAiCmdInfoOpen int
BuildTime string
2024-04-23 17:40:14 -07:00
Renderers map[string]int
}
type ActivityType struct {
Day string `json:"day"`
Uploaded bool `json:"-"`
TData TelemetryData `json:"tdata"`
TzName string `json:"tzname"`
TzOffset int `json:"tzoffset"`
ClientVersion string `json:"clientversion"`
ClientArch string `json:"clientarch"`
BuildTime string `json:"buildtime"`
DefaultShell string `json:"defaultshell"`
OSRelease string `json:"osrelease"`
}
type TelemetryData struct {
2024-04-23 17:40:14 -07:00
NumCommands int `json:"numcommands"`
ActiveMinutes int `json:"activeminutes"`
FgMinutes int `json:"fgminutes"`
OpenMinutes int `json:"openminutes"`
ClickShared int `json:"clickshared,omitempty"`
HistoryView int `json:"historyview,omitempty"`
BookmarksView int `json:"bookmarksview,omitempty"`
NumConns int `json:"numconns"`
NumWorkspaces int `json:"numworkspaces"`
NumTabs int `json:"numtabs"`
NewTab int `json:"newtab"`
NumStartup int `json:"numstartup,omitempty"`
NumShutdown int `json:"numshutdown,omitempty"`
NumAIChatOpen int `json:"numaichatopen,omitempty"`
NumHistoryOpen int `json:"numhistoryopen,omitempty"`
ReinitBashErrors int `json:"reinitbasherrors,omitempty"`
ReinitZshErrors int `json:"reinitzsherrors,omitempty"`
Renderers map[string]int `json:"renderers,omitempty"`
}
func (tdata TelemetryData) Value() (driver.Value, error) {
return dbutil.QuickValueJson(tdata)
}
func (tdata *TelemetryData) Scan(val interface{}) error {
return dbutil.QuickScanJson(tdata, val)
}
2024-04-23 17:40:14 -07:00
func IsAllowedRenderer(renderer string) bool {
return allowedRenderers[renderer]
}
// Wraps UpdateCurrentActivity, spawns goroutine, and logs errors
func GoUpdateActivityWrap(update ActivityUpdate, debugStr string) {
go func() {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
err := UpdateActivity(ctx, update)
if err != nil {
// ignore error, just log, since this is not critical
log.Printf("error updating current activity (%s): %v\n", debugStr, err)
}
}()
}
func GetCurDayStr() string {
now := time.Now()
dayStr := now.Format("2006-01-02")
return dayStr
}
2024-04-05 22:42:22 -07:00
func GetRelDayStr(relDays int) string {
now := time.Now()
dayStr := now.AddDate(0, 0, relDays).Format("2006-01-02")
return dayStr
}
// accepts a custom format string to return a daystr
// can be either a prefix, a delta, or a prefix w/ a delta
// if no prefix is given, "today" is assumed
// examples: today-2d, bow, bom+1m-1d (that's end of the month), 2024-04-01+1w
//
// prefixes:
//
// yyyy-mm-dd
// today
// yesterday
// bom (beginning of month)
// bow (beginning of week -- sunday)
//
// deltas:
//
// +[n]d, -[n]d (e.g. +1d, -5d)
// +[n]w, -[n]w (e.g. +2w)
// +[n]m, -[n]m (e.g. -1m)
// deltas can be combined e.g. +1w-2d
func GetCustomDayStr(format string) (string, error) {
m := customDayStrRe.FindStringSubmatch(format)
if m == nil {
return "", fmt.Errorf("invalid daystr format")
}
prefix, deltas := m[1], m[2]
if prefix == "" {
prefix = "today"
}
var rtnTime time.Time
now := time.Now()
switch prefix {
case "today":
rtnTime = now
case "yesterday":
rtnTime = now.AddDate(0, 0, -1)
case "bom":
rtnTime = time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
case "bow":
weekday := now.Weekday()
if weekday == time.Sunday {
rtnTime = now
} else {
rtnTime = now.AddDate(0, 0, -int(weekday))
}
default:
m = daystrRe.FindStringSubmatch(prefix)
if m == nil {
return "", fmt.Errorf("invalid prefix format")
}
year, month, day := m[1], m[2], m[3]
yearInt, monthInt, dayInt := atoiNoErr(year), atoiNoErr(month), atoiNoErr(day)
if yearInt == 0 || monthInt == 0 || dayInt == 0 {
return "", fmt.Errorf("invalid prefix format")
}
rtnTime = time.Date(yearInt, time.Month(monthInt), dayInt, 0, 0, 0, 0, now.Location())
}
for _, delta := range regexp.MustCompile(`[+-]\d+[dwm]`).FindAllString(deltas, -1) {
deltaVal, err := strconv.Atoi(delta[1 : len(delta)-1])
if err != nil {
return "", fmt.Errorf("invalid delta format")
}
if delta[0] == '-' {
deltaVal = -deltaVal
}
switch delta[len(delta)-1] {
case 'd':
rtnTime = rtnTime.AddDate(0, 0, deltaVal)
case 'w':
rtnTime = rtnTime.AddDate(0, 0, deltaVal*7)
case 'm':
rtnTime = rtnTime.AddDate(0, deltaVal, 0)
}
}
return rtnTime.Format("2006-01-02"), nil
}
func atoiNoErr(str string) int {
val, err := strconv.Atoi(str)
if err != nil {
return 0
}
return val
}
2024-04-23 17:40:14 -07:00
func UpdateFeActivityWrap(feActivity *scpacket.FeActivityPacketType) {
update := ActivityUpdate{}
for key, val := range feActivity.Activity {
if key == "aichat-open" {
update.FeAIChatOpen = val
} else if key == "history-open" {
update.FeHistoryOpen = val
2024-05-29 11:53:16 +08:00
} else if key == "aicmdinfo-open" {
update.FeAiCmdInfoOpen = val
2024-04-23 17:40:14 -07:00
} else {
log.Printf("unknown feactivity key: %s\n", key)
}
}
GoUpdateActivityWrap(update, "feactivity")
}
2024-04-05 22:42:22 -07:00
var customDayStrRe = regexp.MustCompile(`^((?:\d{4}-\d{2}-\d{2})|today|yesterday|bom|bow)?((?:[+-]\d+[dwm])*)$`)
var daystrRe = regexp.MustCompile(`^(\d{4})-(\d{2})-(\d{2})$`)
2024-04-23 17:40:14 -07:00
func UpdateActivity(ctx context.Context, update ActivityUpdate) error {
now := time.Now()
dayStr := GetCurDayStr()
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
var tdata TelemetryData
query := `SELECT tdata FROM activity WHERE day = ?`
found := tx.Get(&tdata, query, dayStr)
if !found {
query = `INSERT INTO activity (day, uploaded, tdata, tzname, tzoffset, clientversion, clientarch, buildtime, osrelease)
VALUES (?, 0, ?, ?, ?, ?, ? , ? , ?)`
tzName, tzOffset := now.Zone()
if len(tzName) > MaxTzNameLen {
tzName = tzName[0:MaxTzNameLen]
}
tx.Exec(query, dayStr, tdata, tzName, tzOffset, scbase.WaveVersion, scbase.ClientArch(), scbase.BuildTime, scbase.UnameKernelRelease())
}
tdata.NumCommands += update.NumCommands
tdata.FgMinutes += update.FgMinutes
tdata.ActiveMinutes += update.ActiveMinutes
tdata.OpenMinutes += update.OpenMinutes
tdata.ClickShared += update.ClickShared
tdata.HistoryView += update.HistoryView
tdata.BookmarksView += update.BookmarksView
tdata.ReinitBashErrors += update.ReinitBashErrors
tdata.ReinitZshErrors += update.ReinitZshErrors
2024-04-23 17:40:14 -07:00
tdata.NewTab += update.NewTab
tdata.NumStartup += update.Startup
tdata.NumShutdown += update.Shutdown
tdata.NumAIChatOpen += update.FeAIChatOpen
tdata.NumHistoryOpen += update.FeHistoryOpen
if update.NumConns > 0 {
tdata.NumConns = update.NumConns
}
2024-04-23 17:40:14 -07:00
if update.NumWorkspaces > 0 {
tdata.NumWorkspaces = update.NumWorkspaces
}
if update.NumTabs > 0 {
tdata.NumTabs = update.NumTabs
}
if len(update.Renderers) > 0 {
if tdata.Renderers == nil {
tdata.Renderers = make(map[string]int)
}
for key, val := range update.Renderers {
tdata.Renderers[key] += val
}
}
query = `UPDATE activity
SET tdata = ?,
clientversion = ?,
buildtime = ?
WHERE day = ?`
tx.Exec(query, tdata, scbase.WaveVersion, scbase.BuildTime, dayStr)
return nil
})
if txErr != nil {
return txErr
}
return nil
}
func GetNonUploadedActivity(ctx context.Context) ([]*ActivityType, error) {
var rtn []*ActivityType
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `SELECT * FROM activity WHERE uploaded = 0 ORDER BY day DESC LIMIT 30`
tx.Select(&rtn, query)
return nil
})
if txErr != nil {
return nil, txErr
}
return rtn, nil
}
// note, will not mark the current day as uploaded
func MarkActivityAsUploaded(ctx context.Context, activityArr []*ActivityType) error {
dayStr := GetCurDayStr()
txErr := sstore.WithTx(ctx, func(tx *sstore.TxWrap) error {
query := `UPDATE activity SET uploaded = 1 WHERE day = ?`
for _, activity := range activityArr {
if activity.Day == dayStr {
continue
}
tx.Exec(query, activity.Day)
}
return nil
})
return txErr
}