mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
Remove telemetry
This commit is contained in:
parent
fb06108fcb
commit
988a0d6401
86 changed files with 91 additions and 3115 deletions
|
|
@ -26,7 +26,6 @@ func GenerateWshClient() error {
|
|||
gogen.GenerateBoilerplate(&buf, "wshclient", []string{
|
||||
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes",
|
||||
"github.com/wavetermdev/waveterm/pkg/baseds",
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata",
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom",
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj",
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig",
|
||||
|
|
|
|||
|
|
@ -22,19 +22,13 @@ import (
|
|||
"github.com/wavetermdev/waveterm/pkg/filestore"
|
||||
"github.com/wavetermdev/waveterm/pkg/jobcontroller"
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
|
||||
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/wshfs"
|
||||
"github.com/wavetermdev/waveterm/pkg/secretstore"
|
||||
"github.com/wavetermdev/waveterm/pkg/service"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/envutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/sigutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wcloud"
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||
"github.com/wavetermdev/waveterm/pkg/wcore"
|
||||
"github.com/wavetermdev/waveterm/pkg/web"
|
||||
|
|
@ -44,7 +38,6 @@ import (
|
|||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshremote"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshserver"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/wslconn"
|
||||
"github.com/wavetermdev/waveterm/pkg/wstore"
|
||||
|
||||
"net/http"
|
||||
|
|
@ -55,15 +48,8 @@ import (
|
|||
var WaveVersion = "0.0.0"
|
||||
var BuildTime = "0"
|
||||
|
||||
const InitialTelemetryWait = 10 * time.Second
|
||||
const TelemetryTick = 2 * time.Minute
|
||||
const TelemetryInterval = 4 * time.Hour
|
||||
const TelemetryInitialCountsWait = 5 * time.Second
|
||||
const TelemetryCountsInterval = 1 * time.Hour
|
||||
const BackupCleanupTick = 2 * time.Minute
|
||||
const BackupCleanupInterval = 4 * time.Hour
|
||||
const InitialDiagnosticWait = 5 * time.Minute
|
||||
const DiagnosticTick = 10 * time.Minute
|
||||
|
||||
var shutdownOnce sync.Once
|
||||
|
||||
|
|
@ -81,8 +67,6 @@ func doShutdown(reason string) {
|
|||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelFn()
|
||||
go blockcontroller.StopAllBlockControllersForShutdown()
|
||||
shutdownActivityUpdate()
|
||||
sendTelemetryWrapper()
|
||||
// TODO deal with flush in progress
|
||||
clearTempFiles()
|
||||
filestore.WFS.FlushCache(ctx)
|
||||
|
|
@ -118,74 +102,6 @@ func startConfigWatcher() {
|
|||
}
|
||||
}
|
||||
|
||||
func telemetryLoop() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("telemetryLoop", recover())
|
||||
}()
|
||||
var nextSend int64
|
||||
time.Sleep(InitialTelemetryWait)
|
||||
for {
|
||||
if time.Now().Unix() > nextSend {
|
||||
nextSend = time.Now().Add(TelemetryInterval).Unix()
|
||||
sendTelemetryWrapper()
|
||||
}
|
||||
time.Sleep(TelemetryTick)
|
||||
}
|
||||
}
|
||||
|
||||
func diagnosticLoop() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("diagnosticLoop", recover())
|
||||
}()
|
||||
if os.Getenv("WAVETERM_NOPING") != "" {
|
||||
log.Printf("WAVETERM_NOPING set, disabling diagnostic ping\n")
|
||||
return
|
||||
}
|
||||
var lastSentDate string
|
||||
time.Sleep(InitialDiagnosticWait)
|
||||
for {
|
||||
currentDate := time.Now().Format("2006-01-02")
|
||||
if lastSentDate == "" || lastSentDate != currentDate {
|
||||
if sendDiagnosticPing() {
|
||||
lastSentDate = currentDate
|
||||
}
|
||||
}
|
||||
time.Sleep(DiagnosticTick)
|
||||
}
|
||||
}
|
||||
|
||||
func sendDiagnosticPing() bool {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelFn()
|
||||
|
||||
rpcClient := wshclient.GetBareRpcClient()
|
||||
isOnline, err := wshclient.NetworkOnlineCommand(rpcClient, &wshrpc.RpcOpts{Route: "electron", Timeout: 2000})
|
||||
if err != nil || !isOnline {
|
||||
return false
|
||||
}
|
||||
clientId := wstore.GetClientId()
|
||||
usageTelemetry := telemetry.IsTelemetryEnabled()
|
||||
wcloud.SendDiagnosticPing(ctx, clientId, usageTelemetry)
|
||||
return true
|
||||
}
|
||||
|
||||
func setupTelemetryConfigHandler() {
|
||||
watcher := wconfig.GetWatcher()
|
||||
if watcher == nil {
|
||||
return
|
||||
}
|
||||
currentConfig := watcher.GetFullConfig()
|
||||
currentTelemetryEnabled := currentConfig.Settings.TelemetryEnabled
|
||||
|
||||
watcher.RegisterUpdateHandler(func(newConfig wconfig.FullConfigType) {
|
||||
newTelemetryEnabled := newConfig.Settings.TelemetryEnabled
|
||||
if newTelemetryEnabled != currentTelemetryEnabled {
|
||||
currentTelemetryEnabled = newTelemetryEnabled
|
||||
wcore.GoSendNoTelemetryUpdate(newTelemetryEnabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func backupCleanupLoop() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("backupCleanupLoop", recover())
|
||||
|
|
@ -203,185 +119,6 @@ func backupCleanupLoop() {
|
|||
}
|
||||
}
|
||||
|
||||
func panicTelemetryHandler(panicName string) {
|
||||
activity := wshrpc.ActivityUpdate{NumPanics: 1}
|
||||
err := telemetry.UpdateActivity(context.Background(), activity)
|
||||
if err != nil {
|
||||
log.Printf("error updating activity (panicTelemetryHandler): %v\n", err)
|
||||
}
|
||||
telemetry.RecordTEvent(context.Background(), telemetrydata.MakeTEvent("debug:panic", telemetrydata.TEventProps{
|
||||
PanicType: panicName,
|
||||
}))
|
||||
}
|
||||
|
||||
func sendTelemetryWrapper() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("sendTelemetryWrapper", recover())
|
||||
}()
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancelFn()
|
||||
beforeSendActivityUpdate(ctx)
|
||||
clientId := wstore.GetClientId()
|
||||
err := wcloud.SendAllTelemetry(clientId)
|
||||
if err != nil {
|
||||
log.Printf("[error] sending telemetry: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func updateTelemetryCounts(lastCounts telemetrydata.TEventProps) telemetrydata.TEventProps {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelFn()
|
||||
var props telemetrydata.TEventProps
|
||||
props.CountBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx)
|
||||
props.CountTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
|
||||
props.CountWindows, _ = wstore.DBGetCount[*waveobj.Window](ctx)
|
||||
props.CountWorkspaces, _, _ = wstore.DBGetWSCounts(ctx)
|
||||
props.CountSSHConn = conncontroller.GetNumSSHHasConnected()
|
||||
props.CountWSLConn = wslconn.GetNumWSLHasConnected()
|
||||
props.CountJobs = jobcontroller.GetNumJobsRunning()
|
||||
props.CountJobsConnected = jobcontroller.GetNumJobsConnected()
|
||||
props.CountViews, _ = wstore.DBGetBlockViewCounts(ctx)
|
||||
|
||||
fullConfig := wconfig.GetWatcher().GetFullConfig()
|
||||
customWidgets := fullConfig.CountCustomWidgets()
|
||||
customAIPresets := fullConfig.CountCustomAIPresets()
|
||||
customSettings := wconfig.CountCustomSettings()
|
||||
customAIModes := fullConfig.CountCustomAIModes()
|
||||
|
||||
props.UserSet = &telemetrydata.TEventUserProps{
|
||||
SettingsCustomWidgets: customWidgets,
|
||||
SettingsCustomAIPresets: customAIPresets,
|
||||
SettingsCustomSettings: customSettings,
|
||||
SettingsCustomAIModes: customAIModes,
|
||||
}
|
||||
|
||||
secretsCount, err := secretstore.CountSecrets()
|
||||
if err == nil {
|
||||
props.UserSet.SettingsSecretsCount = secretsCount
|
||||
}
|
||||
|
||||
if utilfn.CompareAsMarshaledJson(props, lastCounts) {
|
||||
return lastCounts
|
||||
}
|
||||
tevent := telemetrydata.MakeTEvent("app:counts", props)
|
||||
err = telemetry.RecordTEvent(ctx, tevent)
|
||||
if err != nil {
|
||||
log.Printf("error recording counts tevent: %v\n", err)
|
||||
}
|
||||
return props
|
||||
}
|
||||
|
||||
func updateTelemetryCountsLoop() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("updateTelemetryCountsLoop", recover())
|
||||
}()
|
||||
var nextSend int64
|
||||
var lastCounts telemetrydata.TEventProps
|
||||
time.Sleep(TelemetryInitialCountsWait)
|
||||
for {
|
||||
if time.Now().Unix() > nextSend {
|
||||
nextSend = time.Now().Add(TelemetryCountsInterval).Unix()
|
||||
lastCounts = updateTelemetryCounts(lastCounts)
|
||||
}
|
||||
time.Sleep(TelemetryTick)
|
||||
}
|
||||
}
|
||||
|
||||
func beforeSendActivityUpdate(ctx context.Context) {
|
||||
activity := wshrpc.ActivityUpdate{}
|
||||
activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
|
||||
activity.NumBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx)
|
||||
activity.Blocks, _ = wstore.DBGetBlockViewCounts(ctx)
|
||||
activity.NumWindows, _ = wstore.DBGetCount[*waveobj.Window](ctx)
|
||||
activity.NumSSHConn = conncontroller.GetNumSSHHasConnected()
|
||||
activity.NumWSLConn = wslconn.GetNumWSLHasConnected()
|
||||
activity.NumWSNamed, activity.NumWS, _ = wstore.DBGetWSCounts(ctx)
|
||||
err := telemetry.UpdateActivity(ctx, activity)
|
||||
if err != nil {
|
||||
log.Printf("error updating before activity: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func startupActivityUpdate(firstLaunch bool) {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("startupActivityUpdate", recover())
|
||||
}()
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelFn()
|
||||
activity := wshrpc.ActivityUpdate{Startup: 1}
|
||||
err := telemetry.UpdateActivity(ctx, activity) // set at least one record into activity (don't use go routine wrap here)
|
||||
if err != nil {
|
||||
log.Printf("error updating startup activity: %v\n", err)
|
||||
}
|
||||
autoUpdateChannel := telemetry.AutoUpdateChannel()
|
||||
autoUpdateEnabled := telemetry.IsAutoUpdateEnabled()
|
||||
shellType, shellVersion, shellErr := shellutil.DetectShellTypeAndVersion()
|
||||
if shellErr != nil {
|
||||
shellType = "error"
|
||||
shellVersion = ""
|
||||
}
|
||||
userSetOnce := &telemetrydata.TEventUserProps{
|
||||
ClientInitialVersion: "v" + WaveVersion,
|
||||
}
|
||||
tosTs := telemetry.GetTosAgreedTs()
|
||||
var cohortTime time.Time
|
||||
if tosTs > 0 {
|
||||
cohortTime = time.UnixMilli(tosTs)
|
||||
} else {
|
||||
cohortTime = time.Now()
|
||||
}
|
||||
cohortMonth := cohortTime.Format("2006-01")
|
||||
year, week := cohortTime.ISOWeek()
|
||||
cohortISOWeek := fmt.Sprintf("%04d-W%02d", year, week)
|
||||
userSetOnce.CohortMonth = cohortMonth
|
||||
userSetOnce.CohortISOWeek = cohortISOWeek
|
||||
fullConfig := wconfig.GetWatcher().GetFullConfig()
|
||||
props := telemetrydata.TEventProps{
|
||||
UserSet: &telemetrydata.TEventUserProps{
|
||||
ClientVersion: "v" + wavebase.WaveVersion,
|
||||
ClientBuildTime: wavebase.BuildTime,
|
||||
ClientArch: wavebase.ClientArch(),
|
||||
ClientOSRelease: wavebase.UnameKernelRelease(),
|
||||
ClientIsDev: wavebase.IsDevMode(),
|
||||
ClientPackageType: wavebase.ClientPackageType(),
|
||||
ClientMacOSVersion: wavebase.ClientMacOSVersion(),
|
||||
AutoUpdateChannel: autoUpdateChannel,
|
||||
AutoUpdateEnabled: autoUpdateEnabled,
|
||||
LocalShellType: shellType,
|
||||
LocalShellVersion: shellVersion,
|
||||
SettingsTransparent: fullConfig.Settings.WindowTransparent,
|
||||
},
|
||||
UserSetOnce: userSetOnce,
|
||||
}
|
||||
if firstLaunch {
|
||||
props.AppFirstLaunch = true
|
||||
}
|
||||
tevent := telemetrydata.MakeTEvent("app:startup", props)
|
||||
err = telemetry.RecordTEvent(ctx, tevent)
|
||||
if err != nil {
|
||||
log.Printf("error recording startup event: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func shutdownActivityUpdate() {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancelFn()
|
||||
activity := wshrpc.ActivityUpdate{Shutdown: 1}
|
||||
err := telemetry.UpdateActivity(ctx, activity) // do NOT use the go routine wrap here (this needs to be synchronous)
|
||||
if err != nil {
|
||||
log.Printf("error updating shutdown activity: %v\n", err)
|
||||
}
|
||||
err = telemetry.TruncateActivityTEventForShutdown(ctx)
|
||||
if err != nil {
|
||||
log.Printf("error truncating activity t-event for shutdown: %v\n", err)
|
||||
}
|
||||
tevent := telemetrydata.MakeTEvent("app:shutdown", telemetrydata.TEventProps{})
|
||||
err = telemetry.RecordTEvent(ctx, tevent)
|
||||
if err != nil {
|
||||
log.Printf("error recording shutdown event: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func createMainWshClient() {
|
||||
rpc := wshserver.GetMainRpcClient()
|
||||
wshutil.DefaultRouter.RegisterTrustedLeaf(rpc, wshutil.DefaultRoute)
|
||||
|
|
@ -404,10 +141,6 @@ func grabAndRemoveEnvVars() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = wcloud.CacheAndRemoveEnvVars()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove WAVETERM env vars that leak from prod => dev
|
||||
os.Unsetenv("WAVETERM_CLIENTID")
|
||||
|
|
@ -524,7 +257,6 @@ func main() {
|
|||
log.Printf("error initializing wstore: %v\n", err)
|
||||
return
|
||||
}
|
||||
panichandler.PanicTelemetryHandler = panicTelemetryHandler
|
||||
go func() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("InitCustomShellStartupFiles", recover())
|
||||
|
|
@ -565,12 +297,7 @@ func main() {
|
|||
aiusechat.InitAIModeConfigWatcher()
|
||||
maybeStartPprofServer()
|
||||
go stdinReadWatch()
|
||||
go telemetryLoop()
|
||||
go diagnosticLoop()
|
||||
setupTelemetryConfigHandler()
|
||||
go updateTelemetryCountsLoop()
|
||||
go backupCleanupLoop()
|
||||
go startupActivityUpdate(firstLaunch) // must be after startConfigWatcher()
|
||||
blocklogger.InitBlockLogger()
|
||||
jobcontroller.InitJobController()
|
||||
blockcontroller.InitBlockController()
|
||||
|
|
|
|||
|
|
@ -24,24 +24,11 @@ var debugBlockIdsCmd = &cobra.Command{
|
|||
Hidden: true,
|
||||
}
|
||||
|
||||
var debugSendTelemetryCmd = &cobra.Command{
|
||||
Use: "send-telemetry",
|
||||
Short: "send telemetry",
|
||||
RunE: debugSendTelemetryRun,
|
||||
Hidden: true,
|
||||
}
|
||||
|
||||
func init() {
|
||||
debugCmd.AddCommand(debugBlockIdsCmd)
|
||||
debugCmd.AddCommand(debugSendTelemetryCmd)
|
||||
rootCmd.AddCommand(debugCmd)
|
||||
}
|
||||
|
||||
func debugSendTelemetryRun(cmd *cobra.Command, args []string) error {
|
||||
err := wshclient.SendTelemetryCommand(RpcClient, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func debugBlockIdsRun(cmd *cobra.Command, args []string) error {
|
||||
oref, err := resolveBlockArg()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ var fileListCmd = &cobra.Command{
|
|||
Short: "list files",
|
||||
Long: "List files in a directory. By default, lists files in the current directory." + UriHelpText,
|
||||
Example: " wsh file ls wsh://user@ec2/home/user/",
|
||||
RunE: activityWrap("file", fileListRun),
|
||||
RunE: fileListRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
|
|
@ -100,7 +100,7 @@ var fileCatCmd = &cobra.Command{
|
|||
Long: "Display the contents of a file." + UriHelpText,
|
||||
Example: " wsh file cat wsh://user@ec2/home/user/config.txt",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileCatRun),
|
||||
RunE: fileCatRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ var fileInfoCmd = &cobra.Command{
|
|||
Long: "Show information about a file." + UriHelpText,
|
||||
Example: " wsh file info wsh://user@ec2/home/user/config.txt",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileInfoRun),
|
||||
RunE: fileInfoRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ var fileRmCmd = &cobra.Command{
|
|||
Long: "Remove a file." + UriHelpText,
|
||||
Example: " wsh file rm wsh://user@ec2/home/user/config.txt",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileRmRun),
|
||||
RunE: fileRmRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ var fileWriteCmd = &cobra.Command{
|
|||
Long: "Write stdin into a file, buffering input (10MB total file size limit)." + UriHelpText,
|
||||
Example: " echo 'hello' | wsh file write ./greeting.txt",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileWriteRun),
|
||||
RunE: fileWriteRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +140,7 @@ var fileAppendCmd = &cobra.Command{
|
|||
Long: "Append stdin to a file, buffering input (10MB total file size limit)." + UriHelpText,
|
||||
Example: " tail -f log.txt | wsh file append ./app.log",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: activityWrap("file", fileAppendRun),
|
||||
RunE: fileAppendRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ var fileCpCmd = &cobra.Command{
|
|||
Long: "Copy files between different storage systems." + UriHelpText,
|
||||
Example: " wsh file cp wsh://user@ec2/home/user/config.txt ./local-config.txt\n wsh file cp ./local-config.txt wsh://user@ec2/home/user/config.txt",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: activityWrap("file", fileCpRun),
|
||||
RunE: fileCpRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ var fileMvCmd = &cobra.Command{
|
|||
Long: "Move files between different storage systems. The source file will be deleted once the operation completes successfully." + UriHelpText,
|
||||
Example: " wsh file mv wsh://user@ec2/home/user/config.txt ./local-config.txt\n wsh file mv ./local-config.txt wsh://user@ec2/home/user/config.txt",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: activityWrap("file", fileMvRun),
|
||||
RunE: fileMvRun,
|
||||
PreRunE: preRunSetupRpcClient,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -213,22 +213,7 @@ func getTabIdFromEnv() string {
|
|||
return os.Getenv("WAVETERM_TABID")
|
||||
}
|
||||
|
||||
// this will send wsh activity to the client running on *your* local machine (it does not contact any wave cloud infrastructure)
|
||||
// if you've turned off telemetry in your local client, this data never gets sent to us
|
||||
// no parameters or timestamps are sent, as you can see below, it just sends the name of the command (and if there was an error)
|
||||
// (e.g. "wsh ai ..." would send "ai")
|
||||
// this helps us understand which commands are actually being used so we know where to concentrate our effort
|
||||
func sendActivity(wshCmdName string, success bool) {
|
||||
if RpcClient == nil || wshCmdName == "" {
|
||||
return
|
||||
}
|
||||
dataMap := make(map[string]int)
|
||||
dataMap[wshCmdName] = 1
|
||||
if !success {
|
||||
dataMap[wshCmdName+"#"+"error"] = 1
|
||||
}
|
||||
wshclient.WshActivityCommand(RpcClient, dataMap, nil)
|
||||
}
|
||||
func sendActivity(wshCmdName string, success bool) {}
|
||||
|
||||
// Execute executes the root command.
|
||||
func Execute() {
|
||||
|
|
|
|||
2
db/migrations-wstore/000012_drop_telemetry.down.sql
Normal file
2
db/migrations-wstore/000012_drop_telemetry.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- This migration cannot be reversed
|
||||
-- Telemetry tables have been permanently removed
|
||||
3
db/migrations-wstore/000012_drop_telemetry.up.sql
Normal file
3
db/migrations-wstore/000012_drop_telemetry.up.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
-- Drop telemetry tables
|
||||
DROP TABLE IF EXISTS db_tevent;
|
||||
DROP TABLE IF EXISTS db_activity;
|
||||
|
|
@ -115,7 +115,6 @@ wsh editconfig
|
|||
| window:savelastwindow | bool | when `true`, the last window that is closed is preserved and is reopened the next time the app is launched (defaults to `true`) |
|
||||
| window:confirmonclose | bool | when `true`, a prompt will ask a user to confirm that they want to close a window if it has an unsaved workspace with more than one tab (defaults to `true`) |
|
||||
| window:dimensions | string | set the default dimensions for new windows using the format "WIDTHxHEIGHT" (e.g. "1920x1080"). when a new window is created, these dimensions will be automatically applied. The width and height values should be specified in pixels. |
|
||||
| telemetry:enabled | bool | set to enable/disable telemetry |
|
||||
|
||||
For reference, this is the current default configuration (v0.14.0):
|
||||
|
||||
|
|
@ -149,7 +148,6 @@ For reference, this is the current default configuration (v0.14.0):
|
|||
"window:magnifiedblockblursecondarypx": 2,
|
||||
"window:confirmclose": true,
|
||||
"window:savelastwindow": true,
|
||||
"telemetry:enabled": true,
|
||||
"term:bellsound": false,
|
||||
"term:bellindicator": false,
|
||||
"term:osc52": "always",
|
||||
|
|
|
|||
|
|
@ -56,15 +56,3 @@ If you've installed via Snap, you can use the following command:
|
|||
```sh
|
||||
sudo snap install waveterm --classic --beta
|
||||
```
|
||||
|
||||
## Can I use Wave AI without enabling telemetry?
|
||||
|
||||
<VersionBadge version="v0.13.1" noLeftMargin={true}/>
|
||||
|
||||
Yes! Wave AI is normally disabled when telemetry is not enabled. However, you can enable Wave AI features without telemetry by configuring your own custom AI model (either a local model or using your own API key).
|
||||
|
||||
To enable Wave AI without telemetry:
|
||||
1. Configure a custom AI mode (see [Wave AI documentation](./waveai-modes))
|
||||
2. Set `waveai:defaultmode` to your custom mode's key in your Wave settings
|
||||
|
||||
Once you've completed both steps, Wave AI will be enabled and you can use it completely privately without telemetry. This allows you to use local models like Ollama or your own API keys with providers like OpenAI, OpenRouter, or others.
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ Other References:
|
|||
- [Configuration](./config)
|
||||
- [Custom Widgets](./customwidgets)
|
||||
- [Full wsh reference](./wsh-reference)
|
||||
- [Telemetry](./telemetry)
|
||||
- [FAQ](./faq)
|
||||
- [Release Notes](./releasenotes)
|
||||
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ This release focuses on significant Windows platform improvements, Wave AI visua
|
|||
|
||||
**Wave AI Updates:**
|
||||
- **Refreshed Visual Design** - Complete UI refresh removing blue accents and adding transparency support for better integration with custom backgrounds
|
||||
- **BYOK Without Telemetry** - Wave AI now works with bring-your-own-key and local models without requiring telemetry to be enabled
|
||||
- **BYOK Support** - Wave AI now works with bring-your-own-key and local models
|
||||
- [bugfix] Fixed tool type "function" compatibility with providers like Mistral
|
||||
|
||||
**Terminal Improvements:**
|
||||
|
|
@ -376,7 +376,6 @@ Lots of other features and bug fixes as well:
|
|||
- New block splitting support -- Use Cmd-D and Cmd-Shift-D to split horizontally and vertically. For more control you can use Ctrl-Shift-S and then Up/Down/Left/Right to split in the given direction.
|
||||
- Delete block (without removing it from the layout). You can use Ctrl-Shift-D to remove a block, while keeping it in the layout. you can then launch a new widget in its place.
|
||||
- `wsh file` now supports copying files between your local machine, remote machines, and to/from S3
|
||||
- New analytics framework (event based as opposed to counter based). See Telemetry Docs for more information.
|
||||
- Web bookmarks! Edit in your bookmarks.json file, can open them in the web widget using Cmd+O
|
||||
- Edits to your ai.json presets file will now take effect _immediately_ in AI widgets
|
||||
- Much better error handling and messaging when errors occur in the preview or editor widget
|
||||
|
|
@ -502,7 +501,6 @@ New minor release that introduces Wave's connected computing extensions. We've i
|
|||
- `wsh file` operations (cat, write, append, rm, info, cp, and ls) -- [Docs](https://docs.waveterm.dev/wsh-reference#file)
|
||||
- Improved golang panic handling to prevent backend crashes
|
||||
- Improved SSH config logging and fixes a reused connection bug
|
||||
- Updated telemetry to track additional counters
|
||||
- New configuration settings (under "window:magnifiedblock") to control magnified block margins and display
|
||||
- New block/zone aliases (client, global, block, workspace, temp)
|
||||
- `wsh ai` file attachments are now rendered with special handling in the AI block
|
||||
|
|
@ -687,7 +685,6 @@ Minor cleanup release.
|
|||
- fix number parsing for certain config file values
|
||||
- add link to docs site
|
||||
- add new back button for directory view
|
||||
- telemetry fixes
|
||||
|
||||
### v0.8.0 — Sep 20, 2024
|
||||
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
---
|
||||
id: "telemetry-old"
|
||||
title: "Legacy Telemetry"
|
||||
sidebar_class_name: hidden
|
||||
---
|
||||
|
||||
Wave Terminal collects telemetry data to help us track feature use, direct future product efforts, and generate aggregate metrics on Wave's popularity and usage. We do not collect or store any PII (personal identifiable information) and all metric data is only associated with and aggregated using your randomly generated _ClientId_. You may opt out of collection at any time.
|
||||
|
||||
If you would like to turn telemetry on or off, the first opportunity is a button on the initial welcome page. After this, it can be turned off by adding `"telemetry:enabled": false` to the `config/settings.json` file. It can alternatively be turned on by adding `"telemetry:enabled": true` to the `config/settings.json` file.
|
||||
|
||||
:::info
|
||||
|
||||
You can also change your telemetry setting by running the wsh command:
|
||||
|
||||
```
|
||||
wsh setconfig telemetry:enabled=true
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
---
|
||||
|
||||
## Sending Telemetry
|
||||
|
||||
Provided that telemetry is enabled, it is sent 10 seconds after Waveterm is first booted and then again every 4 hours thereafter. It can also be sent in response to a few special cases listed below. When telemetry is sent, it is grouped into individual days as determined by your time zone. Any data from a previous day is marked as `Uploaded` so it will not need to be sent again.
|
||||
|
||||
### Sending Once Telemetry is Enabled
|
||||
|
||||
As soon as telemetry is enabled, a telemetry update is sent regardless of how long it has been since the last send. This does not reset the usual timer for telemetry sends.
|
||||
|
||||
### Notifying that Telemetry is Disabled
|
||||
|
||||
As soon as telemetry is disabled, Waveterm sends a special update that notifies us of this change. See [When Telemetry is Turned Off](#when-telemetry-is-turned-off) for more info. The timer still runs in the background but no data is sent.
|
||||
|
||||
### When Waveterm is Closed
|
||||
|
||||
Provided that telemetry is enabled, it will be sent when Waveterm is closed.
|
||||
|
||||
---
|
||||
|
||||
## Telemetry Data
|
||||
|
||||
When telemetry is active, we collect the following data. It is stored in the `telemetry.TelemetryData` type in the source code.
|
||||
|
||||
| Name | Description |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ActiveMinutes | The number of minutes that the user has actively used Waveterm on a given day. This requires the terminal window to be in focus while the user is actively interacting with it. |
|
||||
| FgMinutes | The number of minutes that Waveterm has been in the foreground on a given day. This requires the terminal window to be in focus regardless of user interaction. |
|
||||
| OpenMinutes | The number of minutes that Waveterm has been open on a given day. This only requires that the terminal is open, even if the window is out of focus. |
|
||||
| NumBlocks | The number of existing blocks open on a given day |
|
||||
| NumTabs | The number of existing tabs open on a given day. |
|
||||
| NewTab | The number of new tabs created on a given day |
|
||||
| NumWindows | The number of existing windows open on a given day. |
|
||||
| NumWS | The number of existing workspaces on a given day. |
|
||||
| NumWSNamed | The number of named workspaces on a give day. |
|
||||
| NewTab | The number of new tabs opened on a given day. |
|
||||
| NumStartup | The number of times waveterm has been started on a given day. |
|
||||
| NumShutdown | The number of times waveterm has been shut down on a given day. |
|
||||
| SetTabTheme | The number of times the tab theme is changed from the context menu |
|
||||
| NumMagnify | The number of times any block is magnified |
|
||||
| NumPanics | The number of backend (golang) panics caught in the current day |
|
||||
| NumAIReqs | The number of AI requests made in the current day |
|
||||
| NumSSHConn | The number of distinct SSH connections that have been made to distinct hosts |
|
||||
| NumWSLConns | The number of distinct WSL connections that have been made to distinct distros |
|
||||
| Renderers | The number of new block views of each type are open on a given day. |
|
||||
| WshCmds | The number of wsh commands of each type run on a given day |
|
||||
| Blocks | The number of blocks of different view types open on a given day |
|
||||
| Conn | The number of successful remote connections made (and errors) on a given day |
|
||||
|
||||
## Associated Data
|
||||
|
||||
In addition to the telemetry data collected, the following is also reported. It is stored in the `telemetry.ActivityType` type in the source code.
|
||||
|
||||
| Name | Description |
|
||||
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Day | The date the telemetry is associated with. It does not include the time. |
|
||||
| Uploaded | A boolean that indicates if the telemetry for this day is finalized. It is false during the day the telemetry is associated with, but gets set true at the first telemetry upload after that. Once it is true, the data for that particular day will not be sent up with the telemetry any more. |
|
||||
| TzName | The code for the timezone the user's OS is reporting (e.g. PST, GMT, JST) |
|
||||
| TzOffset | The offset for the timezone the user's OS is reporting (e.g. -08:00, +00:00, +09:00) |
|
||||
| ClientVersion | Which version of Waveterm is installed. |
|
||||
| ClientArch | This includes the user's operating system (e.g. linux or darwin) and architecture (e.g. x86_64 or arm64). It does not include data for any Connections at this time. |
|
||||
| BuildTime | This serves as a more accurate version number that keeps track of when we built the version. It has no bearing on when that version was installed by you. |
|
||||
| OSRelease | This lists the version of the operating system the user has installed. |
|
||||
| Displays | Display resolutions (added in v0.9.3 to help us understand what screen resolutions to optimize for) |
|
||||
|
||||
## Telemetry Metadata
|
||||
|
||||
Lastly, some data is sent along with the telemetry that describes how to classify it. It is stored in the `wcloud.TelemetryInputType` in the source code.
|
||||
|
||||
| Name | Description |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| UserId | Currently Unused. This is an anonymous UUID intended for use in future features. |
|
||||
| ClientId | This is an anonymous UUID created when Waveterm is first launched. It is used for telemetry and sending prompts to Open AI. |
|
||||
| AppType | This is used to differentiate the current version of waveterm from the legacy app. |
|
||||
| AutoUpdateEnabled | Whether or not auto update is turned on. |
|
||||
| AutoUpdateChannel | The type of auto update in use. This specifically refers to whether a latest or beta channel is selected. |
|
||||
| CurDay | The current day (in your time zone) when telemetry is sent. It does not include the time of day. |
|
||||
|
||||
## Geo Data
|
||||
|
||||
We do not store IP addresses in our telemetry table. However, CloudFlare passes us Geo-Location headers. We store these two header values:
|
||||
|
||||
| Name | Description |
|
||||
| ------------ | ----------------------------------------------------------------- |
|
||||
| CFCountry | 2-letter country code (e.g. "US", "FR", or "JP") |
|
||||
| CFRegionCode | region code (often a provence, region, or state within a country) |
|
||||
|
||||
---
|
||||
|
||||
## When Telemetry is Turned Off
|
||||
|
||||
When a user disables telemetry, Waveterm sends a notification that their anonymous _ClientId_ has had its telemetry disabled. This is done with the `wcloud.NoTelemetryInputType` type in the source code. Beyond that, no further information is sent unless telemetry is turned on again. If it is turned on again, the previous 30 days of telemetry will be sent.
|
||||
|
||||
---
|
||||
|
||||
## A Note on IP Addresses
|
||||
|
||||
Telemetry is uploaded via https, which means your IP address is known to the telemetry server. We **do not** store your IP address in our telemetry table and **do not** associate it with your _ClientId_.
|
||||
|
||||
---
|
||||
|
||||
## Previously Collected Telemetry Data
|
||||
|
||||
While we believe the data we collect with telemetry is fairly minimal, we cannot make that decision for every user. If you ever change your mind about what has been collected previously, you may request that your data be deleted by emailing us at [support@waveterm.dev](mailto:support@waveterm.dev). If you do, we will need your _ClientId_ to remove it.
|
||||
|
||||
---
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
For a summary of the above, you can take a look at our [Privacy Policy](https://www.waveterm.dev/privacy).
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
---
|
||||
sidebar_position: 100
|
||||
title: Telemetry
|
||||
id: "telemetry"
|
||||
---
|
||||
|
||||
## tl;dr
|
||||
|
||||
Wave Terminal collects telemetry data to help us track feature use, direct future product efforts, and generate aggregate metrics on Wave's popularity and usage. We do NOT collect personal information (PII), keystrokes, file contents, AI prompts, IP addresses, hostnames, or commands. We attach all information to an anonymous, randomly generated _ClientId_ (UUID). You may opt out of collection at any time.
|
||||
|
||||
Here's a quick summary of what is collected:
|
||||
|
||||
- Basic App/System Info - OS, architecture, app version, update settings
|
||||
- Usage Metrics - App start/shutdown, active minutes, foreground time, tab/block counts/usage
|
||||
- Feature Interactions - When you create tabs, run commands, change settings, etc.
|
||||
- Display Info - Monitor resolution, number of displays
|
||||
- Connection Events - SSH/WSL connection attempts (but NOT hostnames/IPs)
|
||||
- Wave AI Usage - Model/provider selection, token counts, request metrics, latency (but NOT prompts or responses)
|
||||
- Error Reports - Crash/panic events with minimal debugging info, but no stack traces or detailed errors
|
||||
|
||||
Telemetry can be disabled at any time in settings. If not disabled it is sent on startup, on shutdown, and every 4-hours.
|
||||
|
||||
## How to Disable Telemetry
|
||||
|
||||
Telemetry can be enabled or disabled on the initial welcome screen when Wave first starts. After setup, telemetry can be disabled by setting the `telemetry:enabled` key to `false` in Wave’s general configuration file. It can also be disabled using the CLI command `wsh setconfig telemetry:enabled=false`.
|
||||
|
||||
:::info
|
||||
|
||||
This document outlines the current telemetry system as of v0.11.1. As of v0.12.5, Wave Terminal no longer sends legacy telemetry. The previous telemetry documentation can be found in our [Legacy Telemetry Documentation](./telemetry-old.mdx) for historical reference.
|
||||
|
||||
:::
|
||||
|
||||
## Diagnostics Ping
|
||||
|
||||
Wave sends a small, anonymous diagnostics ping after the app has been running for a short time and at most once per day thereafter. This is used to estimate active installs and understand which versions are still in use, so we can make informed decisions about ongoing support and deprecations.
|
||||
|
||||
The ping includes only: your Wave version, OS/CPU arch, local date (yyyy-mm-dd, no timezone or clock time), your randomly generated anonymous client ID, and whether usage telemetry is enabled or disabled.
|
||||
|
||||
It does not include usage data, commands, files, or any telemetry events.
|
||||
|
||||
This ping is intentionally separate from telemetry so Wave can count active installs. If you'd like to disable it, set the WAVETERM_NOPING environment variable.
|
||||
|
||||
## Sending Telemetry
|
||||
|
||||
Provided that telemetry is enabled, it is sent shortly after Wave is first launched and then again every 4 hours thereafter. It can also be sent in response to a few special cases listed below. When telemetry is sent, events are marked as sent to prevent duplicate transmissions.
|
||||
|
||||
### Sending Once Telemetry is Enabled
|
||||
|
||||
As soon as telemetry is enabled, a telemetry update is sent regardless of how long it has been since the last send. This does not reset the usual timer for telemetry sends.
|
||||
|
||||
### When Wave is Closed
|
||||
|
||||
Provided that telemetry is enabled, it will be sent when Waveterm is closed.
|
||||
|
||||
## Event Types and Properties
|
||||
|
||||
Wave collects the event types and properties described in the summary above. As we add features, new events and properties may be added to track their usage.
|
||||
|
||||
For the complete, current list of all telemetry events and properties, see the source code: [telemetrydata.go](https://github.com/wavetermdev/waveterm/blob/main/pkg/telemetry/telemetrydata/telemetrydata.go)
|
||||
|
||||
## GDPR Opt-Out Compliance
|
||||
|
||||
When telemetry is disabled, Wave sends a single minimal opt-out record associated with the anonymous client ID, recording that telemetry was turned off and when it occurred. This record is retained for compliance purposes. After that, no telemetry or usage data is sent.
|
||||
|
||||
## Deleting Your Data
|
||||
|
||||
If you want your previously collected telemetry data deleted, email us at support (at) waveterm.dev with your _ClientId_ and we'll remove it.
|
||||
|
||||
## Privacy Policy
|
||||
|
||||
For a summary of the above, you can take a look at our [Privacy Policy](https://www.waveterm.dev/privacy).
|
||||
|
|
@ -76,10 +76,6 @@ wsh setconfig waveai:defaultmode="ollama-llama"
|
|||
|
||||
This will make the specified mode the default selection when opening Wave AI features.
|
||||
|
||||
:::note
|
||||
Wave AI normally requires telemetry to be enabled. However, if you configure your own custom model (local or BYOK) and set `waveai:defaultmode` to that custom mode's key, you will not receive telemetry requirement messages. This allows you to use Wave AI features completely privately with your own models. <VersionBadge version="v0.13.1"/>
|
||||
:::
|
||||
|
||||
### Hiding Wave Cloud Modes
|
||||
|
||||
If you prefer to use only your local or custom models and want to hide Wave's cloud AI modes from the mode dropdown, set `waveai:showcloudmodes` to `false`:
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ See the [**Local Models & BYOK guide**](./waveai-modes.mdx) for complete configu
|
|||
**Default Wave AI Service:**
|
||||
- Messages are proxied through the Wave Cloud AI service (powered by OpenAI's APIs). Please refer to OpenAI's privacy policy for details on how they handle your data.
|
||||
- Wave does not store your chats, attachments, or use them for training
|
||||
- Usage counters included in anonymous telemetry
|
||||
- File access requires explicit approval
|
||||
|
||||
**Local Models & BYOK:**
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// for activity updates
|
||||
let wasActive = true;
|
||||
let wasInFg = true;
|
||||
let globalIsQuitting = false;
|
||||
let globalIsStarting = true;
|
||||
let globalIsRelaunching = false;
|
||||
let forceQuit = false;
|
||||
let userConfirmedQuit = false;
|
||||
let termCommandsRun = 0;
|
||||
let termCommandsRemote = 0;
|
||||
let termCommandsWsl = 0;
|
||||
let termCommandsDurable = 0;
|
||||
|
||||
export function setWasActive(val: boolean) {
|
||||
wasActive = val;
|
||||
}
|
||||
|
||||
export function setWasInFg(val: boolean) {
|
||||
wasInFg = val;
|
||||
}
|
||||
|
||||
export function getActivityState(): { wasActive: boolean; wasInFg: boolean } {
|
||||
return { wasActive, wasInFg };
|
||||
}
|
||||
|
||||
export function setGlobalIsQuitting(val: boolean) {
|
||||
globalIsQuitting = val;
|
||||
}
|
||||
|
||||
export function getGlobalIsQuitting(): boolean {
|
||||
return globalIsQuitting;
|
||||
}
|
||||
|
||||
export function setGlobalIsStarting(val: boolean) {
|
||||
globalIsStarting = val;
|
||||
}
|
||||
|
||||
export function getGlobalIsStarting(): boolean {
|
||||
return globalIsStarting;
|
||||
}
|
||||
|
||||
export function setGlobalIsRelaunching(val: boolean) {
|
||||
globalIsRelaunching = val;
|
||||
}
|
||||
|
||||
export function getGlobalIsRelaunching(): boolean {
|
||||
return globalIsRelaunching;
|
||||
}
|
||||
|
||||
export function setForceQuit(val: boolean) {
|
||||
forceQuit = val;
|
||||
}
|
||||
|
||||
export function getForceQuit(): boolean {
|
||||
return forceQuit;
|
||||
}
|
||||
|
||||
export function setUserConfirmedQuit(val: boolean) {
|
||||
userConfirmedQuit = val;
|
||||
}
|
||||
|
||||
export function getUserConfirmedQuit(): boolean {
|
||||
return userConfirmedQuit;
|
||||
}
|
||||
|
||||
export function incrementTermCommandsRun() {
|
||||
termCommandsRun++;
|
||||
}
|
||||
|
||||
export function getAndClearTermCommandsRun(): number {
|
||||
const count = termCommandsRun;
|
||||
termCommandsRun = 0;
|
||||
return count;
|
||||
}
|
||||
|
||||
export function incrementTermCommandsRemote() {
|
||||
termCommandsRemote++;
|
||||
}
|
||||
|
||||
export function getAndClearTermCommandsRemote(): number {
|
||||
const count = termCommandsRemote;
|
||||
termCommandsRemote = 0;
|
||||
return count;
|
||||
}
|
||||
|
||||
export function incrementTermCommandsWsl() {
|
||||
termCommandsWsl++;
|
||||
}
|
||||
|
||||
export function getAndClearTermCommandsWsl(): number {
|
||||
const count = termCommandsWsl;
|
||||
termCommandsWsl = 0;
|
||||
return count;
|
||||
}
|
||||
|
||||
export function incrementTermCommandsDurable() {
|
||||
termCommandsDurable++;
|
||||
}
|
||||
|
||||
export function getAndClearTermCommandsDurable(): number {
|
||||
const count = termCommandsDurable;
|
||||
termCommandsDurable = 0;
|
||||
return count;
|
||||
}
|
||||
|
|
@ -12,13 +12,6 @@ import { RpcApi } from "../frontend/app/store/wshclientapi";
|
|||
import { getWebServerEndpoint } from "../frontend/util/endpoints";
|
||||
import * as keyutil from "../frontend/util/keyutil";
|
||||
import { fireAndForget, parseDataUrl } from "../frontend/util/util";
|
||||
import {
|
||||
incrementTermCommandsDurable,
|
||||
incrementTermCommandsRemote,
|
||||
incrementTermCommandsRun,
|
||||
incrementTermCommandsWsl,
|
||||
setWasActive,
|
||||
} from "./emain-activity";
|
||||
import { callWithOriginalXdgCurrentDesktopAsync, unamePlatform } from "./emain-platform";
|
||||
import { getWaveTabViewByWebContentsId } from "./emain-tabview";
|
||||
import { handleCtrlShiftState } from "./emain-util";
|
||||
|
|
@ -242,10 +235,6 @@ export function initIpcHandlers() {
|
|||
tabView?.setKeyboardChordMode(true);
|
||||
});
|
||||
|
||||
electron.ipcMain.handle("set-is-active", () => {
|
||||
setWasActive(true);
|
||||
});
|
||||
|
||||
const fac = new FastAverageColor();
|
||||
electron.ipcMain.on("update-window-controls-overlay", async (event, rect: Dimensions) => {
|
||||
if (unamePlatform === "darwin") return;
|
||||
|
|
@ -318,22 +307,6 @@ export function initIpcHandlers() {
|
|||
console.log("fe-log", logStr);
|
||||
});
|
||||
|
||||
electron.ipcMain.on(
|
||||
"increment-term-commands",
|
||||
(event, opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) => {
|
||||
incrementTermCommandsRun();
|
||||
if (opts?.isRemote) {
|
||||
incrementTermCommandsRemote();
|
||||
}
|
||||
if (opts?.isWsl) {
|
||||
incrementTermCommandsWsl();
|
||||
}
|
||||
if (opts?.isDurable) {
|
||||
incrementTermCommandsDurable();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
electron.ipcMain.on("native-paste", (event) => {
|
||||
event.sender.paste();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { Rectangle, shell, WebContentsView } from "electron";
|
|||
import { createNewWaveWindow, getWaveWindowById } from "emain/emain-window";
|
||||
import path from "path";
|
||||
import { configureAuthKeyRequestInjection } from "./authkey";
|
||||
import { setWasActive } from "./emain-activity";
|
||||
import { getElectronAppBasePath, isDevVite, unamePlatform } from "./emain-platform";
|
||||
import {
|
||||
decreaseZoomLevel,
|
||||
|
|
@ -326,7 +325,6 @@ export async function getOrCreateWebViewForTab(waveWindowId: string, tabId: stri
|
|||
const waveEvent = adaptFromElectronKeyEvent(input);
|
||||
// console.log("WIN bie", tabView.waveTabId.substring(0, 8), waveEvent.type, waveEvent.code);
|
||||
handleCtrlShiftState(tabView.webContents, waveEvent);
|
||||
setWasActive(true);
|
||||
if (input.type == "keyDown" && tabView.keyboardChordMode) {
|
||||
e.preventDefault();
|
||||
tabView.setKeyboardChordMode(false);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import * as child_process from "node:child_process";
|
|||
import * as readline from "readline";
|
||||
import { WebServerEndpointVarName, WSServerEndpointVarName } from "../frontend/util/endpoints";
|
||||
import { AuthKey, WaveAuthKeyEnv } from "./authkey";
|
||||
import { setForceQuit, setUserConfirmedQuit } from "./emain-activity";
|
||||
import { setForceQuit, setUserConfirmedQuit } from "./emain";
|
||||
import {
|
||||
getElectronAppResourcesPath,
|
||||
getElectronAppUnpackedBasePath,
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ import {
|
|||
getGlobalIsQuitting,
|
||||
getGlobalIsRelaunching,
|
||||
setGlobalIsRelaunching,
|
||||
setWasActive,
|
||||
setWasInFg,
|
||||
} from "./emain-activity";
|
||||
} from "./emain";
|
||||
import { log } from "./emain-log";
|
||||
import { getElectronAppBasePath, isDev, unamePlatform } from "./emain-platform";
|
||||
import { getOrCreateWebViewForTab, getWaveTabViewByWebContentsId, WaveTabView } from "./emain-tabview";
|
||||
|
|
@ -285,8 +283,6 @@ export class WaveBrowserWindow extends BaseWindow {
|
|||
focusedWaveWindow = this; // eslint-disable-line @typescript-eslint/no-this-alias
|
||||
console.log("focus win", this.waveWindowId);
|
||||
fireAndForget(() => ClientService.FocusWindow(this.waveWindowId));
|
||||
setWasInFg(true);
|
||||
setWasActive(true);
|
||||
setTimeout(() => globalEvents.emit("windows-updated"), 50);
|
||||
});
|
||||
this.on("blur", () => {
|
||||
|
|
|
|||
184
emain/emain.ts
184
emain/emain.ts
|
|
@ -9,22 +9,6 @@ import * as services from "../frontend/app/store/services";
|
|||
import { initElectronWshrpc, shutdownWshrpc } from "../frontend/app/store/wshrpcutil-base";
|
||||
import { fireAndForget, sleep } from "../frontend/util/util";
|
||||
import { AuthKey, configureAuthKeyRequestInjection } from "./authkey";
|
||||
import {
|
||||
getActivityState,
|
||||
getAndClearTermCommandsDurable,
|
||||
getAndClearTermCommandsRemote,
|
||||
getAndClearTermCommandsRun,
|
||||
getAndClearTermCommandsWsl,
|
||||
getForceQuit,
|
||||
getGlobalIsRelaunching,
|
||||
getUserConfirmedQuit,
|
||||
setForceQuit,
|
||||
setGlobalIsQuitting,
|
||||
setGlobalIsStarting,
|
||||
setUserConfirmedQuit,
|
||||
setWasActive,
|
||||
setWasInFg,
|
||||
} from "./emain-activity";
|
||||
import { initIpcHandlers } from "./emain-ipc";
|
||||
import { log } from "./emain-log";
|
||||
import { initMenuEventSubscriptions, makeAndSetAppMenu, makeDockTaskbar } from "./emain-menu";
|
||||
|
|
@ -61,6 +45,53 @@ const electronApp = electron.app;
|
|||
|
||||
let confirmQuit = true;
|
||||
|
||||
// Lifecycle state management
|
||||
let globalIsQuitting = false;
|
||||
let globalIsStarting = true;
|
||||
let globalIsRelaunching = false;
|
||||
let forceQuit = false;
|
||||
let userConfirmedQuit = false;
|
||||
|
||||
export function setGlobalIsQuitting(val: boolean) {
|
||||
globalIsQuitting = val;
|
||||
}
|
||||
|
||||
export function getGlobalIsQuitting(): boolean {
|
||||
return globalIsQuitting;
|
||||
}
|
||||
|
||||
export function setGlobalIsStarting(val: boolean) {
|
||||
globalIsStarting = val;
|
||||
}
|
||||
|
||||
export function getGlobalIsStarting(): boolean {
|
||||
return globalIsStarting;
|
||||
}
|
||||
|
||||
export function setGlobalIsRelaunching(val: boolean) {
|
||||
globalIsRelaunching = val;
|
||||
}
|
||||
|
||||
export function getGlobalIsRelaunching(): boolean {
|
||||
return globalIsRelaunching;
|
||||
}
|
||||
|
||||
export function setForceQuit(val: boolean) {
|
||||
forceQuit = val;
|
||||
}
|
||||
|
||||
export function getForceQuit(): boolean {
|
||||
return forceQuit;
|
||||
}
|
||||
|
||||
export function setUserConfirmedQuit(val: boolean) {
|
||||
userConfirmedQuit = val;
|
||||
}
|
||||
|
||||
export function getUserConfirmedQuit(): boolean {
|
||||
return userConfirmedQuit;
|
||||
}
|
||||
|
||||
const waveDataDir = getWaveDataDir();
|
||||
const waveConfigDir = getWaveConfigDir();
|
||||
|
||||
|
|
@ -120,125 +151,6 @@ function handleWSEvent(evtMsg: WSEventType) {
|
|||
});
|
||||
}
|
||||
|
||||
// we try to set the primary display as index [0]
|
||||
function getActivityDisplays(): ActivityDisplayType[] {
|
||||
const displays = electron.screen.getAllDisplays();
|
||||
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
||||
const rtn: ActivityDisplayType[] = [];
|
||||
for (const display of displays) {
|
||||
const adt = {
|
||||
width: display.size.width,
|
||||
height: display.size.height,
|
||||
dpr: display.scaleFactor,
|
||||
internal: display.internal,
|
||||
};
|
||||
if (display.id === primaryDisplay?.id) {
|
||||
rtn.unshift(adt);
|
||||
} else {
|
||||
rtn.push(adt);
|
||||
}
|
||||
}
|
||||
return rtn;
|
||||
}
|
||||
|
||||
async function sendDisplaysTDataEvent() {
|
||||
const displays = getActivityDisplays();
|
||||
if (displays.length === 0) {
|
||||
return;
|
||||
}
|
||||
const props: TEventProps = {};
|
||||
props["display:count"] = displays.length;
|
||||
props["display:height"] = displays[0].height;
|
||||
props["display:width"] = displays[0].width;
|
||||
props["display:dpr"] = displays[0].dpr;
|
||||
props["display:all"] = displays;
|
||||
try {
|
||||
await RpcApi.RecordTEventCommand(
|
||||
ElectronWshClient,
|
||||
{
|
||||
event: "app:display",
|
||||
props,
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("error sending display tdata event", e);
|
||||
}
|
||||
}
|
||||
|
||||
function logActiveState() {
|
||||
fireAndForget(async () => {
|
||||
const astate = getActivityState();
|
||||
const activity: ActivityUpdate = { openminutes: 1 };
|
||||
const ww = focusedWaveWindow;
|
||||
const activeTabView = ww?.activeTabView;
|
||||
const isWaveAIOpen = activeTabView?.isWaveAIOpen ?? false;
|
||||
|
||||
if (astate.wasInFg) {
|
||||
activity.fgminutes = 1;
|
||||
}
|
||||
if (astate.wasActive) {
|
||||
activity.activeminutes = 1;
|
||||
}
|
||||
activity.displays = getActivityDisplays();
|
||||
|
||||
const termCmdCount = getAndClearTermCommandsRun();
|
||||
if (termCmdCount > 0) {
|
||||
activity.termcommandsrun = termCmdCount;
|
||||
}
|
||||
const termCmdRemoteCount = getAndClearTermCommandsRemote();
|
||||
const termCmdWslCount = getAndClearTermCommandsWsl();
|
||||
const termCmdDurableCount = getAndClearTermCommandsDurable();
|
||||
|
||||
const props: TEventProps = {
|
||||
"activity:activeminutes": activity.activeminutes,
|
||||
"activity:fgminutes": activity.fgminutes,
|
||||
"activity:openminutes": activity.openminutes,
|
||||
};
|
||||
if (termCmdCount > 0) {
|
||||
props["activity:termcommandsrun"] = termCmdCount;
|
||||
}
|
||||
if (termCmdRemoteCount > 0) {
|
||||
props["activity:termcommands:remote"] = termCmdRemoteCount;
|
||||
}
|
||||
if (termCmdWslCount > 0) {
|
||||
props["activity:termcommands:wsl"] = termCmdWslCount;
|
||||
}
|
||||
if (termCmdDurableCount > 0) {
|
||||
props["activity:termcommands:durable"] = termCmdDurableCount;
|
||||
}
|
||||
if (astate.wasActive && isWaveAIOpen) {
|
||||
props["activity:waveaiactiveminutes"] = 1;
|
||||
}
|
||||
if (astate.wasInFg && isWaveAIOpen) {
|
||||
props["activity:waveaifgminutes"] = 1;
|
||||
}
|
||||
|
||||
try {
|
||||
await RpcApi.ActivityCommand(ElectronWshClient, activity, { noresponse: true });
|
||||
await RpcApi.RecordTEventCommand(
|
||||
ElectronWshClient,
|
||||
{
|
||||
event: "app:activity",
|
||||
props,
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
} catch (e) {
|
||||
console.log("error logging active state", e);
|
||||
} finally {
|
||||
setWasInFg(ww?.isFocused() ?? false);
|
||||
setWasActive(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// this isn't perfect, but gets the job done without being complicated
|
||||
function runActiveTimer() {
|
||||
logActiveState();
|
||||
setTimeout(runActiveTimer, 60000);
|
||||
}
|
||||
|
||||
function hideWindowWithCatch(window: WaveBrowserWindow) {
|
||||
if (window == null) {
|
||||
return;
|
||||
|
|
@ -408,8 +320,6 @@ async function appMain() {
|
|||
}
|
||||
ensureHotSpareTab(fullConfig);
|
||||
await relaunchBrowserWindows();
|
||||
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
|
||||
setTimeout(sendDisplaysTDataEvent, 5000);
|
||||
|
||||
makeAndSetAppMenu();
|
||||
makeDockTaskbar();
|
||||
|
|
|
|||
|
|
@ -57,12 +57,9 @@ contextBridge.exposeInMainWorld("api", {
|
|||
captureScreenshot: (rect: Rectangle) => ipcRenderer.invoke("capture-screenshot", rect),
|
||||
setKeyboardChordMode: () => ipcRenderer.send("set-keyboard-chord-mode"),
|
||||
setWaveAIOpen: (isOpen: boolean) => ipcRenderer.send("set-waveai-open", isOpen),
|
||||
incrementTermCommands: (opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) =>
|
||||
ipcRenderer.send("increment-term-commands", opts),
|
||||
nativePaste: () => ipcRenderer.send("native-paste"),
|
||||
doRefresh: () => ipcRenderer.send("do-refresh"),
|
||||
getPathForFile: (file: File): string => webUtils.getPathForFile(file),
|
||||
saveTextFile: (fileName: string, content: string) => ipcRenderer.invoke("save-text-file", fileName, content),
|
||||
setIsActive: () => ipcRenderer.invoke("set-is-active"),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import YAML from "yaml";
|
|||
import { RpcApi } from "../frontend/app/store/wshclientapi";
|
||||
import { isDev } from "../frontend/util/isdev";
|
||||
import { fireAndForget } from "../frontend/util/util";
|
||||
import { setUserConfirmedQuit } from "./emain-activity";
|
||||
import { setUserConfirmedQuit } from "./emain";
|
||||
import { delay } from "./emain-util";
|
||||
import { focusedWaveWindow, getAllWaveWindows } from "./emain-window";
|
||||
import { ElectronWshClient } from "./emain-wsh";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import { cn, makeIconClass } from "@/util/util";
|
||||
import { memo, useState } from "react";
|
||||
import { WaveAIModel } from "./waveai-model";
|
||||
|
||||
interface AIFeedbackButtonsProps {
|
||||
messageText: string;
|
||||
|
|
@ -19,9 +18,6 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps)
|
|||
if (thumbsDownClicked) {
|
||||
setThumbsDownClicked(false);
|
||||
}
|
||||
if (!thumbsUpClicked) {
|
||||
WaveAIModel.getInstance().handleAIFeedback("good");
|
||||
}
|
||||
};
|
||||
|
||||
const handleThumbsDown = () => {
|
||||
|
|
@ -29,9 +25,6 @@ export const AIFeedbackButtons = memo(({ messageText }: AIFeedbackButtonsProps)
|
|||
if (thumbsUpClicked) {
|
||||
setThumbsUpClicked(false);
|
||||
}
|
||||
if (!thumbsDownClicked) {
|
||||
WaveAIModel.getInstance().handleAIFeedback("bad");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Tooltip } from "@/app/element/tooltip";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { cn, fireAndForget, makeIconClass, sortByDisplayOrder } from "@/util/util";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo, useMemo, useRef, useState } from "react";
|
||||
|
|
@ -70,14 +68,6 @@ export const AIModeDropdown = memo(() => {
|
|||
|
||||
const handleConfigureClick = () => {
|
||||
fireAndForget(async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "action:other",
|
||||
props: { "action:type": "waveai:configuremodes:contextmenu" },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
await model.openWaveAIConfig();
|
||||
setIsOpen(false);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,23 +99,6 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo
|
|||
|
||||
menu.push({ type: "separator" });
|
||||
|
||||
menu.push({
|
||||
label: "Configure Modes",
|
||||
click: () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "action:other",
|
||||
props: {
|
||||
"action:type": "waveai:configuremodes:contextmenu",
|
||||
},
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
model.openWaveAIConfig();
|
||||
},
|
||||
});
|
||||
|
||||
if (model.canCloseWaveAIPanel()) {
|
||||
menu.push({ type: "separator" });
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import { AIPanelMessages } from "./aipanelmessages";
|
|||
import { AIRateLimitStrip } from "./airatelimitstrip";
|
||||
import { WaveUIMessage } from "./aitypes";
|
||||
import { BYOKAnnouncement } from "./byokannouncement";
|
||||
import { TelemetryRequiredMessage } from "./telemetryrequired";
|
||||
import { WaveAIModel } from "./waveai-model";
|
||||
|
||||
const AIBlockMask = memo(() => {
|
||||
|
|
@ -198,12 +197,11 @@ AIErrorMessage.displayName = "AIErrorMessage";
|
|||
|
||||
const ConfigChangeModeFixer = memo(() => {
|
||||
const model = WaveAIModel.getInstance();
|
||||
const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false;
|
||||
const aiModeConfigs = jotai.useAtomValue(model.aiModeConfigs);
|
||||
|
||||
useEffect(() => {
|
||||
model.fixModeAfterConfigChange();
|
||||
}, [telemetryEnabled, aiModeConfigs, model]);
|
||||
}, [aiModeConfigs, model]);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
|
@ -225,14 +223,13 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps
|
|||
const showOverlayBlockNums = jotai.useAtomValue(getSettingsKeyAtom("app:showoverlayblocknums")) ?? true;
|
||||
const isFocused = jotai.useAtomValue(model.isWaveAIFocusedAtom);
|
||||
const focusFollowsCursorMode = jotai.useAtomValue(getSettingsKeyAtom("app:focusfollowscursor")) ?? "off";
|
||||
const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false;
|
||||
const isPanelVisible = jotai.useAtomValue(model.getPanelVisibleAtom());
|
||||
const tabModel = useTabModelMaybe();
|
||||
const [tabBorderColor, tabActiveBorderColor] = useTabBackground(waveEnv, tabModel?.tabId);
|
||||
const aiModelConfigs = jotai.useAtomValue(model.aiModelConfigs);
|
||||
|
||||
const hasCustomModels = aiModelConfigs != null && Object.keys(aiModelConfigs).length > 0;
|
||||
const allowAccess = telemetryEnabled || hasCustomModels;
|
||||
const allowAccess = true;
|
||||
|
||||
const { messages, sendMessage, status, setMessages, error, stop } = useChat<WaveUIMessage>({
|
||||
transport: new DefaultChatTransport({
|
||||
|
|
@ -542,10 +539,7 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps
|
|||
<AIRateLimitStrip />
|
||||
|
||||
<div key="main-content" className="flex-1 flex flex-col min-h-0">
|
||||
{!allowAccess ? (
|
||||
<TelemetryRequiredMessage />
|
||||
) : (
|
||||
<>
|
||||
<>
|
||||
{messages.length === 0 && initialLoadDone ? (
|
||||
<div
|
||||
className="flex-1 overflow-y-auto px-2 pb-2"
|
||||
|
|
@ -568,7 +562,6 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps
|
|||
<AIDroppedFiles model={model} />
|
||||
<AIPanelInput onSubmit={handleSubmit} status={status} model={model} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import { BlockModel } from "@/app/block/block-model";
|
||||
import { Modal } from "@/app/modals/modal";
|
||||
import { recordTEvent } from "@/app/store/global";
|
||||
import { cn, fireAndForget } from "@/util/util";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
|
|
@ -311,7 +310,6 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => {
|
|||
};
|
||||
|
||||
const handleOpenDiff = () => {
|
||||
recordTEvent("waveai:showdiff");
|
||||
fireAndForget(() => WaveAIModel.getInstance().openDiff(toolData.inputfilename, toolData.toolcallid));
|
||||
};
|
||||
|
||||
|
|
@ -341,7 +339,6 @@ const AIToolUse = memo(({ part, isStreaming }: AIToolUseProps) => {
|
|||
Date.now() - toolData.runts < BackupRetentionDays * 24 * 60 * 60 * 1000 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
recordTEvent("waveai:revertfile", { "waveai:action": "revertfile:open" });
|
||||
model.openRestoreBackupModal(toolData.toolcallid);
|
||||
}}
|
||||
className="flex-shrink-0 px-1.5 py-0.5 border border-zinc-600 hover:border-zinc-500 hover:bg-zinc-700 rounded cursor-pointer transition-colors flex items-center gap-1 text-zinc-400"
|
||||
|
|
|
|||
|
|
@ -1,40 +1,15 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { WaveAIModel } from "./waveai-model";
|
||||
|
||||
const BYOKAnnouncement = () => {
|
||||
const model = WaveAIModel.getInstance();
|
||||
|
||||
const handleOpenConfig = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "action:other",
|
||||
props: {
|
||||
"action:type": "waveai:configuremodes:panel",
|
||||
},
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
await model.openWaveAIConfig();
|
||||
};
|
||||
|
||||
const handleViewDocs = () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "action:other",
|
||||
props: {
|
||||
"action:type": "waveai:viewdocs:panel",
|
||||
},
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-blue-900/20 border border-blue-800 rounded-lg p-4 mt-4">
|
||||
<div className="flex items-start gap-3">
|
||||
|
|
@ -56,7 +31,6 @@ const BYOKAnnouncement = () => {
|
|||
href="https://docs.waveterm.dev/waveai-modes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={handleViewDocs}
|
||||
className="text-blue-400! hover:text-blue-300! hover:underline text-sm cursor-pointer transition-colors flex items-center gap-1"
|
||||
>
|
||||
View Docs <i className="fa fa-external-link text-xs"></i>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { Modal } from "@/app/modals/modal";
|
||||
import { recordTEvent } from "@/app/store/global";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo } from "react";
|
||||
import { WaveUIMessagePart } from "./aitypes";
|
||||
|
|
@ -25,12 +24,10 @@ export const RestoreBackupModal = memo(({ part }: RestoreBackupModalProps) => {
|
|||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
recordTEvent("waveai:revertfile", { "waveai:action": "revertfile:confirm" });
|
||||
model.restoreBackup(toolData.toolcallid, toolData.writebackupfilename, toolData.inputfilename);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
recordTEvent("waveai:revertfile", { "waveai:action": "revertfile:cancel" });
|
||||
model.closeRestoreBackupModal();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { cn } from "@/util/util";
|
||||
import { useState } from "react";
|
||||
import { WaveAIModel } from "./waveai-model";
|
||||
|
||||
interface TelemetryRequiredMessageProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const TelemetryRequiredMessage = ({ className }: TelemetryRequiredMessageProps) => {
|
||||
const [isEnabling, setIsEnabling] = useState(false);
|
||||
|
||||
const handleEnableTelemetry = async () => {
|
||||
setIsEnabling(true);
|
||||
try {
|
||||
await RpcApi.WaveAIEnableTelemetryCommand(TabRpcClient);
|
||||
setTimeout(() => {
|
||||
WaveAIModel.getInstance().focusInput();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error("Failed to enable telemetry:", error);
|
||||
setIsEnabling(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col h-full", className)}>
|
||||
<div className="flex-grow"></div>
|
||||
<div className="flex items-center justify-center p-8 text-center">
|
||||
<div className="max-w-md space-y-6">
|
||||
<div className="space-y-4">
|
||||
<i className="fa fa-sparkles text-accent text-5xl"></i>
|
||||
<h2 className="text-2xl font-semibold text-foreground">Wave AI</h2>
|
||||
<p className="text-secondary leading-relaxed">
|
||||
Wave AI is free to use and provides integrated AI chat that can interact with your widgets,
|
||||
help you with code, analyze files, and assist with your terminal workflows.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-900/20 border border-blue-500 rounded-lg p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<i className="fa fa-info-circle text-blue-400 text-lg mt-0.5"></i>
|
||||
<div className="text-left">
|
||||
<div className="text-blue-400 font-medium mb-1">Telemetry keeps Wave AI free</div>
|
||||
<div className="text-secondary text-sm mb-3">
|
||||
<p className="mb-2">
|
||||
To keep Wave AI free for everyone, we require a small amount of <i>anonymous</i>{" "}
|
||||
usage data (app version, feature usage, system info).
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
This helps us block abuse by automated systems and ensure it's used by real
|
||||
people like you.
|
||||
</p>
|
||||
<p className="mb-2">
|
||||
We never collect your files, prompts, keystrokes, hostnames, or personally
|
||||
identifying information. Wave AI is powered by OpenAI's APIs, please refer to
|
||||
OpenAI's privacy policy for details on how they handle your data.
|
||||
</p>
|
||||
<p>
|
||||
For information about BYOK and local model support, see{" "}
|
||||
<a
|
||||
href="https://docs.waveterm.dev/waveai-modes"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="!text-secondary hover:!text-accent/80 cursor-pointer"
|
||||
>
|
||||
https://docs.waveterm.dev/waveai-modes
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleEnableTelemetry}
|
||||
disabled={isEnabling}
|
||||
className="bg-accent/80 hover:bg-accent disabled:bg-accent/50 text-background px-4 py-2 rounded-lg font-medium cursor-pointer disabled:cursor-not-allowed"
|
||||
>
|
||||
{isEnabling ? "Enabling..." : "Enable Telemetry and Continue"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-secondary">
|
||||
<a
|
||||
href="https://waveterm.dev/privacy"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="!text-secondary hover:!text-accent/80 cursor-pointer"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow-[2]"></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
TelemetryRequiredMessage.displayName = "TelemetryRequiredMessage";
|
||||
|
||||
export { TelemetryRequiredMessage };
|
||||
|
|
@ -101,15 +101,7 @@ export class WaveAIModel {
|
|||
});
|
||||
|
||||
this.defaultModeAtom = jotai.atom((get) => {
|
||||
const telemetryEnabled = get(getSettingsKeyAtom("telemetry:enabled")) ?? false;
|
||||
const aiModeConfigs = get(this.aiModeConfigs);
|
||||
if (!telemetryEnabled) {
|
||||
let mode = get(getSettingsKeyAtom("waveai:defaultmode"));
|
||||
if (mode == null || mode.startsWith("waveai@")) {
|
||||
return "unknown";
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
const waveFallback = "waveai@ask";
|
||||
let mode = get(getSettingsKeyAtom("waveai:defaultmode")) ?? waveFallback;
|
||||
const modeExists = aiModeConfigs != null && mode in aiModeConfigs;
|
||||
|
|
@ -385,11 +377,6 @@ export class WaveAIModel {
|
|||
}
|
||||
|
||||
isValidMode(mode: string): boolean {
|
||||
const telemetryEnabled = globalStore.get(getSettingsKeyAtom("telemetry:enabled")) ?? false;
|
||||
if (mode.startsWith("waveai@") && !telemetryEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const aiModeConfigs = globalStore.get(this.aiModeConfigs);
|
||||
if (aiModeConfigs == null || !(mode in aiModeConfigs)) {
|
||||
return false;
|
||||
|
|
@ -601,19 +588,6 @@ export class WaveAIModel {
|
|||
}
|
||||
}
|
||||
|
||||
handleAIFeedback(feedback: "good" | "bad") {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "waveai:feedback",
|
||||
props: {
|
||||
"waveai:feedback": feedback,
|
||||
},
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
}
|
||||
|
||||
requestWaveAIFocus() {
|
||||
FocusManager.getInstance().requestWaveAIFocus();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,7 +286,6 @@ const AppKeyHandlers = () => {
|
|||
const staticKeyDownHandler = keyutil.keydownWrapper(appHandleKeyDown);
|
||||
const staticMouseDownHandler = (e: MouseEvent) => {
|
||||
keyboardMouseDownHandler(e);
|
||||
GlobalModel.getInstance().setIsActive();
|
||||
};
|
||||
document.addEventListener("keydown", staticKeyDownHandler);
|
||||
document.addEventListener("mousedown", staticMouseDownHandler);
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ export type BlockEnv = WaveEnvSubset<{
|
|||
openExternal: WaveEnv["electron"]["openExternal"];
|
||||
};
|
||||
rpc: {
|
||||
ActivityCommand: WaveEnv["rpc"]["ActivityCommand"];
|
||||
ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"];
|
||||
ConnDisconnectCommand: WaveEnv["rpc"]["ConnDisconnectCommand"];
|
||||
ConnConnectCommand: WaveEnv["rpc"]["ConnConnectCommand"];
|
||||
|
|
|
|||
|
|
@ -14,13 +14,11 @@ import { getBlockBadgeAtom } from "@/app/store/badge";
|
|||
import {
|
||||
createBlockSplitHorizontally,
|
||||
createBlockSplitVertically,
|
||||
recordTEvent,
|
||||
refocusNode,
|
||||
WOS,
|
||||
} from "@/app/store/global";
|
||||
import { globalStore } from "@/app/store/jotaiStore";
|
||||
import { uxCloseBlock } from "@/app/store/keymodel";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { useWaveEnv } from "@/app/waveenv/waveenv";
|
||||
import { IconButton } from "@/element/iconbutton";
|
||||
import { NodeModel } from "@/layout/index";
|
||||
|
|
@ -237,10 +235,6 @@ const BlockFrame_Header = ({
|
|||
viewIconUnion = metaFrameIcon ?? viewIconUnion;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (magnified && !preview && !prevMagifiedState.current) {
|
||||
waveEnv.rpc.ActivityCommand(TabRpcClient, { nummagnify: 1 });
|
||||
recordTEvent("action:magnify", { "block:view": viewName });
|
||||
}
|
||||
prevMagifiedState.current = magnified;
|
||||
}, [magnified]);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { computeConnColorNum } from "@/app/block/blockutil";
|
||||
import { recordTEvent } from "@/app/store/global";
|
||||
import { useWaveEnv } from "@/app/waveenv/waveenv";
|
||||
import { IconButton } from "@/element/iconbutton";
|
||||
import * as util from "@/util/util";
|
||||
|
|
@ -30,7 +29,6 @@ export const ConnectionButton = React.memo(
|
|||
const connColorNum = computeConnColorNum(connStatus);
|
||||
let color = `var(--conn-icon-color-${connColorNum})`;
|
||||
const clickHandler = function () {
|
||||
recordTEvent("action:other", { "action:type": "conndropdown", "action:initiator": "mouse" });
|
||||
setConnModalOpen(true);
|
||||
};
|
||||
let titleText = null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright 2026, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { recordTEvent } from "@/app/store/global";
|
||||
import { TermViewModel } from "@/app/view/term/term-model";
|
||||
import { useWaveEnv } from "@/app/waveenv/waveenv";
|
||||
import * as util from "@/util/util";
|
||||
|
|
@ -44,7 +43,6 @@ interface StandardSessionContentProps {
|
|||
|
||||
function StandardSessionContent({ viewModel, onClose }: StandardSessionContentProps) {
|
||||
const handleRestartAsDurable = () => {
|
||||
recordTEvent("action:termdurable", { "action:type": "restartdurable" });
|
||||
onClose();
|
||||
util.fireAndForget(() => viewModel.restartSessionWithDurability(true));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,12 +5,9 @@ import Logo from "@/app/asset/logo.svg";
|
|||
import { OnboardingGradientBg } from "@/app/onboarding/onboarding-common";
|
||||
import { atoms } from "@/app/store/global";
|
||||
import { modalsModel } from "@/app/store/modalmodel";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { isDev } from "@/util/isdev";
|
||||
import { fireAndForget } from "@/util/util";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { Modal } from "./modal";
|
||||
|
||||
interface AboutModalVProps {
|
||||
|
|
@ -89,16 +86,6 @@ const AboutModal = () => {
|
|||
const versionString = `${fullConfig?.version ?? ""} (${isDev() ? "dev-" : ""}${fullConfig?.buildtime ?? ""})`;
|
||||
const updaterChannel = fullConfig?.settings?.["autoupdate:channel"] ?? "latest";
|
||||
|
||||
useEffect(() => {
|
||||
fireAndForget(async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{ event: "action:other", props: { "action:type": "about" } },
|
||||
{ noresponse: true }
|
||||
);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AboutModalV
|
||||
versionString={versionString}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const chatConfigs: ChatConfig[] = [
|
|||
## Architecture at a glance
|
||||
- **Electron main process:** \`emain/*.ts\` configures windows, menus, preload scripts, updater, and ties into the Go backend via local RPC. (\`emain/\`)
|
||||
- **Renderer UI:** React/TS built with Vite, Tailwind. (\`frontend/\`, \`index.html\`, \`electron.vite.config.ts\`)
|
||||
- **Go backend ("wavesrv"):** starts services, web and websocket listeners, telemetry loops, config watcher, local RPC, filestore and SQLite-backed object store. (\`cmd/server/main-server.go\`, \`pkg/*\`)
|
||||
- **Go backend ("wavesrv"):** starts services, web and websocket listeners, config watcher, local RPC, filestore and SQLite-backed object store. (\`cmd/server/main-server.go\`, \`pkg/*\`)
|
||||
- **CLI/helper ("wsh"):** built for multiple OS/arch; used for shell integration and remote operations. (\`cmd/wsh/\`, \`Taskfile.yml build:wsh\`)
|
||||
|
||||
## Key directories
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
import Logo from "@/app/asset/logo.svg";
|
||||
import { EmojiButton } from "@/app/element/emojibutton";
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { useState } from "react";
|
||||
import { CurrentOnboardingVersion } from "./onboarding-common";
|
||||
import { OnboardingFooter } from "./onboarding-features-footer";
|
||||
|
|
@ -23,15 +21,6 @@ export const DurableSessionPage = ({
|
|||
|
||||
const handleFireClick = () => {
|
||||
setFireClicked(!fireClicked);
|
||||
if (!fireClicked) {
|
||||
RpcApi.RecordTEventCommand(TabRpcClient, {
|
||||
event: "onboarding:fire",
|
||||
props: {
|
||||
"onboarding:feature": "durable",
|
||||
"onboarding:version": CurrentOnboardingVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -26,15 +26,6 @@ export const WaveAIPage = ({ onNext, onSkip }: { onNext: () => void; onSkip: ()
|
|||
|
||||
const handleFireClick = () => {
|
||||
setFireClicked(!fireClicked);
|
||||
if (!fireClicked) {
|
||||
RpcApi.RecordTEventCommand(TabRpcClient, {
|
||||
event: "onboarding:fire",
|
||||
props: {
|
||||
"onboarding:feature": "waveai",
|
||||
"onboarding:version": CurrentOnboardingVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -121,15 +112,6 @@ export const MagnifyBlocksPage = ({
|
|||
|
||||
const handleFireClick = () => {
|
||||
setFireClicked(!fireClicked);
|
||||
if (!fireClicked) {
|
||||
RpcApi.RecordTEventCommand(TabRpcClient, {
|
||||
event: "onboarding:fire",
|
||||
props: {
|
||||
"onboarding:feature": "magnify",
|
||||
"onboarding:version": CurrentOnboardingVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -179,15 +161,6 @@ export const FilesPage = ({ onFinish, onPrev }: { onFinish: () => void; onPrev?:
|
|||
|
||||
const handleFireClick = () => {
|
||||
setFireClicked(!fireClicked);
|
||||
if (!fireClicked) {
|
||||
RpcApi.RecordTEventCommand(TabRpcClient, {
|
||||
event: "onboarding:fire",
|
||||
props: {
|
||||
"onboarding:feature": "wsh",
|
||||
"onboarding:version": CurrentOnboardingVersion,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const commands = [
|
||||
|
|
@ -273,12 +246,6 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) =
|
|||
oref: WOS.makeORef("client", clientId),
|
||||
meta: { "onboarding:lastversion": CurrentOnboardingVersion },
|
||||
});
|
||||
RpcApi.RecordTEventCommand(TabRpcClient, {
|
||||
event: "onboarding:start",
|
||||
props: {
|
||||
"onboarding:version": CurrentOnboardingVersion,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleNext = () => {
|
||||
|
|
@ -302,10 +269,6 @@ export const OnboardingFeatures = ({ onComplete }: { onComplete: () => void }) =
|
|||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
RpcApi.RecordTEventCommand(TabRpcClient, {
|
||||
event: "onboarding:skip",
|
||||
props: {},
|
||||
});
|
||||
onComplete();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,6 @@ type StarAskPageProps = {
|
|||
|
||||
export function StarAskPage({ onClose, page = "upgrade" }: StarAskPageProps) {
|
||||
const handleStarClick = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "onboarding:githubstar",
|
||||
props: { "onboarding:githubstar": "star", "onboarding:page": page },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
const clientId = ClientModel.getInstance().clientId;
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("client", clientId),
|
||||
|
|
@ -33,14 +25,6 @@ export function StarAskPage({ onClose, page = "upgrade" }: StarAskPageProps) {
|
|||
};
|
||||
|
||||
const handleAlreadyStarred = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "onboarding:githubstar",
|
||||
props: { "onboarding:githubstar": "already", "onboarding:page": page },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
const clientId = ClientModel.getInstance().clientId;
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("client", clientId),
|
||||
|
|
@ -50,26 +34,10 @@ export function StarAskPage({ onClose, page = "upgrade" }: StarAskPageProps) {
|
|||
};
|
||||
|
||||
const handleRepoLinkClick = () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "action:link",
|
||||
props: { "action:type": "githubrepo", "onboarding:page": page },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
window.open("https://github.com/wavetermdev/waveterm", "_blank");
|
||||
};
|
||||
|
||||
const handleMaybeLater = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "onboarding:githubstar",
|
||||
props: { "onboarding:githubstar": "later", "onboarding:page": page },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
const clientId = ClientModel.getInstance().clientId;
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("client", clientId),
|
||||
|
|
|
|||
|
|
@ -131,14 +131,6 @@ const UpgradeOnboardingMinor = () => {
|
|||
}, []);
|
||||
|
||||
const handleStarClick = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "onboarding:githubstar",
|
||||
props: { "onboarding:githubstar": "star", "onboarding:page": "minorupgrade" },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
const clientId = ClientModel.getInstance().clientId;
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("client", clientId),
|
||||
|
|
@ -149,14 +141,6 @@ const UpgradeOnboardingMinor = () => {
|
|||
};
|
||||
|
||||
const handleAlreadyStarred = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "onboarding:githubstar",
|
||||
props: { "onboarding:githubstar": "already", "onboarding:page": "minorupgrade" },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
const clientId = ClientModel.getInstance().clientId;
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("client", clientId),
|
||||
|
|
@ -166,14 +150,6 @@ const UpgradeOnboardingMinor = () => {
|
|||
};
|
||||
|
||||
const handleMaybeLater = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "onboarding:githubstar",
|
||||
props: { "onboarding:githubstar": "later", "onboarding:page": "minorupgrade" },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
const clientId = ClientModel.getInstance().clientId;
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("client", clientId),
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@ const UpgradeOnboardingModal_v0_13_1_Content = () => {
|
|||
support for custom backgrounds
|
||||
</li>
|
||||
<li>
|
||||
<strong>BYOK Without Telemetry</strong> - Wave AI now works with bring-your-own-key and
|
||||
local models without requiring telemetry
|
||||
<strong>BYOK Support</strong> - Wave AI now works with bring-your-own-key and local models
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,34 +22,21 @@ import { useEffect, useRef, useState } from "react";
|
|||
import { debounce } from "throttle-debounce";
|
||||
|
||||
// Page flow:
|
||||
// init -> (telemetry enabled) -> features
|
||||
// init -> (telemetry disabled) -> notelemetrystar -> features
|
||||
// init -> features
|
||||
|
||||
type PageName = "init" | "notelemetrystar" | "features";
|
||||
type PageName = "init" | "features";
|
||||
|
||||
const pageNameAtom: PrimitiveAtom<PageName> = atom<PageName>("init");
|
||||
|
||||
const InitPage = ({
|
||||
isCompact,
|
||||
telemetryUpdateFn,
|
||||
}: {
|
||||
isCompact: boolean;
|
||||
telemetryUpdateFn: (value: boolean) => Promise<void>;
|
||||
}) => {
|
||||
const telemetrySetting = useSettingsKeyAtom("telemetry:enabled");
|
||||
const clientData = useAtomValue(ClientModel.getInstance().clientAtom);
|
||||
const [telemetryEnabled, setTelemetryEnabled] = useState<boolean>(!!telemetrySetting);
|
||||
const setPageName = useSetAtom(pageNameAtom);
|
||||
|
||||
const handleStarClick = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "onboarding:githubstar",
|
||||
props: { "onboarding:githubstar": "star", "onboarding:page": "init" },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
const clientId = ClientModel.getInstance().clientId;
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("client", clientId),
|
||||
|
|
@ -61,22 +48,10 @@ const InitPage = ({
|
|||
if (!clientData?.tosagreed) {
|
||||
fireAndForget(() => services.ClientService.AgreeTos());
|
||||
}
|
||||
if (telemetryEnabled) {
|
||||
WorkspaceLayoutModel.getInstance().setAIPanelVisible(true);
|
||||
}
|
||||
setPageName(telemetryEnabled ? "features" : "notelemetrystar");
|
||||
WorkspaceLayoutModel.getInstance().setAIPanelVisible(true);
|
||||
setPageName("features");
|
||||
};
|
||||
|
||||
const setTelemetry = (value: boolean) => {
|
||||
fireAndForget(() =>
|
||||
telemetryUpdateFn(value).then(() => {
|
||||
setTelemetryEnabled(value);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const label = telemetryEnabled ? "Enabled" : "Disabled";
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<header
|
||||
|
|
@ -149,34 +124,6 @@ const InitPage = ({
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full items-center gap-[18px]">
|
||||
<div>
|
||||
<i className="text-[32px] text-white/50 fa-solid fa-chart-line"></i>
|
||||
</div>
|
||||
<div className="flex flex-col items-start gap-1 flex-1">
|
||||
<div className="text-secondary leading-5">
|
||||
Anonymous usage data helps us improve features you use.
|
||||
<br />
|
||||
<a
|
||||
className="text-secondary! hover:underline!"
|
||||
target="_blank"
|
||||
href="https://waveterm.dev/privacy"
|
||||
rel="noopener"
|
||||
>
|
||||
Privacy Policy
|
||||
</a>
|
||||
</div>
|
||||
<label className="flex items-center gap-2 cursor-pointer text-secondary">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={telemetryEnabled}
|
||||
onChange={(e) => setTelemetry(e.target.checked)}
|
||||
className="cursor-pointer accent-gray-500"
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
<footer className={`unselectable flex-shrink-0 ${isCompact ? "mt-2" : "mt-5"}`}>
|
||||
|
|
@ -190,79 +137,6 @@ const InitPage = ({
|
|||
);
|
||||
};
|
||||
|
||||
const NoTelemetryStarPage = ({ isCompact }: { isCompact: boolean }) => {
|
||||
const setPageName = useSetAtom(pageNameAtom);
|
||||
|
||||
const handleStarClick = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "onboarding:githubstar",
|
||||
props: { "onboarding:githubstar": "star", "onboarding:page": "notelemetry" },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
const clientId = ClientModel.getInstance().clientId;
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("client", clientId),
|
||||
meta: { "onboarding:githubstar": true },
|
||||
});
|
||||
window.open("https://github.com/wavetermdev/waveterm?ref=not", "_blank");
|
||||
setPageName("features");
|
||||
};
|
||||
|
||||
const handleMaybeLater = async () => {
|
||||
RpcApi.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "onboarding:githubstar",
|
||||
props: { "onboarding:githubstar": "later", "onboarding:page": "notelemetry" },
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
const clientId = ClientModel.getInstance().clientId;
|
||||
await RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
oref: WOS.makeORef("client", clientId),
|
||||
meta: { "onboarding:githubstar": false },
|
||||
});
|
||||
setPageName("features");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<header className={`flex flex-col gap-2 border-b-0 p-0 mt-1 mb-4 w-full unselectable flex-shrink-0`}>
|
||||
<div className={`flex justify-center`}>
|
||||
<Logo />
|
||||
</div>
|
||||
<div className="text-center text-[25px] font-normal text-foreground">Telemetry Disabled ✓</div>
|
||||
</header>
|
||||
<OverlayScrollbarsComponent
|
||||
className="flex-1 overflow-y-auto min-h-0"
|
||||
options={{ scrollbars: { autoHide: "never" } }}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-6 w-full mb-2 unselectable">
|
||||
<div className="text-center text-secondary leading-relaxed max-w-md">
|
||||
<p className="mb-4">No problem, we respect your privacy.</p>
|
||||
<p className="mb-4">
|
||||
But, without usage data, we're flying blind. A GitHub star helps us know Wave is useful and
|
||||
worth maintaining.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
<footer className={`unselectable flex-shrink-0 mt-2`}>
|
||||
<div className="flex flex-row items-center justify-center gap-2.5 [&>button]:!px-5 [&>button]:!py-2 [&>button]:text-sm [&>button]:!h-[37px]">
|
||||
<Button className="outlined green font-[600]" onClick={handleStarClick}>
|
||||
⭐ Star on GitHub
|
||||
</Button>
|
||||
<Button className="outlined grey font-[600]" onClick={handleMaybeLater}>
|
||||
Maybe Later
|
||||
</Button>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FeaturesPage = () => {
|
||||
const [newInstallOnboardingOpen, setNewInstallOnboardingOpen] = useAtom(modalsModel.newInstallOnboardingOpen);
|
||||
|
|
@ -325,10 +199,7 @@ const NewInstallOnboardingModal = () => {
|
|||
let pageComp: React.JSX.Element = null;
|
||||
switch (pageName) {
|
||||
case "init":
|
||||
pageComp = <InitPage isCompact={isCompact} telemetryUpdateFn={(value) => services.ClientService.TelemetryUpdate(value)} />;
|
||||
break;
|
||||
case "notelemetrystar":
|
||||
pageComp = <NoTelemetryStarPage isCompact={isCompact} />;
|
||||
pageComp = <InitPage isCompact={isCompact} />;
|
||||
break;
|
||||
case "features":
|
||||
pageComp = <FeaturesPage />;
|
||||
|
|
@ -351,4 +222,4 @@ const NewInstallOnboardingModal = () => {
|
|||
|
||||
NewInstallOnboardingModal.displayName = "NewInstallOnboardingModal";
|
||||
|
||||
export { InitPage, NewInstallOnboardingModal, NoTelemetryStarPage };
|
||||
export { InitPage, NewInstallOnboardingModal };
|
||||
|
|
|
|||
|
|
@ -4,16 +4,13 @@
|
|||
import * as WOS from "@/app/store/wos";
|
||||
import { ClientModel } from "@/app/store/client-model";
|
||||
import { getApi } from "@/store/global";
|
||||
import * as util from "@/util/util";
|
||||
import { atom, Atom } from "jotai";
|
||||
|
||||
class GlobalModel {
|
||||
private static instance: GlobalModel;
|
||||
static readonly IsActiveThrottleMs = 5000;
|
||||
|
||||
windowId: string;
|
||||
platform: NodeJS.Platform;
|
||||
lastSetIsActiveTs = 0;
|
||||
|
||||
windowDataAtom!: Atom<WaveWindow>;
|
||||
workspaceAtom!: Atom<Workspace>;
|
||||
|
|
@ -50,14 +47,6 @@ class GlobalModel {
|
|||
});
|
||||
}
|
||||
|
||||
setIsActive(): void {
|
||||
const now = Date.now();
|
||||
if (now - this.lastSetIsActiveTs < GlobalModel.IsActiveThrottleMs) {
|
||||
return;
|
||||
}
|
||||
this.lastSetIsActiveTs = now;
|
||||
util.fireAndForget(() => getApi().setIsActive());
|
||||
}
|
||||
}
|
||||
|
||||
export { GlobalModel };
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import {
|
||||
getLayoutModelForStaticTab,
|
||||
LayoutTreeActionType,
|
||||
|
|
@ -671,13 +669,6 @@ function setActiveTab(tabId: string) {
|
|||
getApi().setActiveTab(tabId);
|
||||
}
|
||||
|
||||
function recordTEvent(event: string, props?: TEventProps) {
|
||||
if (isPreviewWindow()) return;
|
||||
if (props == null) {
|
||||
props = {};
|
||||
}
|
||||
RpcApi.RecordTEventCommand(TabRpcClient, { event, props }, { noresponse: true });
|
||||
}
|
||||
|
||||
export {
|
||||
atoms,
|
||||
|
|
@ -713,7 +704,6 @@ export {
|
|||
makeDefaultConnStatus,
|
||||
openLink,
|
||||
readAtom,
|
||||
recordTEvent,
|
||||
refocusNode,
|
||||
registerBlockComponentModel,
|
||||
replaceBlock,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import {
|
|||
getFocusedBlockId,
|
||||
getSettingsKeyAtom,
|
||||
globalStore,
|
||||
recordTEvent,
|
||||
refocusNode,
|
||||
replaceBlock,
|
||||
WOS,
|
||||
|
|
@ -646,7 +645,6 @@ function registerGlobalKeys() {
|
|||
globalKeyMap.set("Cmd:g", () => {
|
||||
const bcm = getBlockComponentModel(getFocusedBlockInStaticTab());
|
||||
if (bcm.openSwitchConnection != null) {
|
||||
recordTEvent("action:other", { "action:type": "conndropdown", "action:initiator": "keyboard" });
|
||||
bcm.openSwitchConnection();
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,9 +62,6 @@ export class ClientServiceType {
|
|||
GetTab(arg1: string): Promise<Tab> {
|
||||
return callBackendService(this?.waveEnv, "client", "GetTab", Array.from(arguments))
|
||||
}
|
||||
TelemetryUpdate(arg2: boolean): Promise<void> {
|
||||
return callBackendService(this?.waveEnv, "client", "TelemetryUpdate", Array.from(arguments))
|
||||
}
|
||||
}
|
||||
|
||||
export const ClientService = new ClientServiceType();
|
||||
|
|
|
|||
|
|
@ -18,12 +18,6 @@ export class RpcApiType {
|
|||
this.mockClient = client;
|
||||
}
|
||||
|
||||
// command "activity" [call]
|
||||
ActivityCommand(client: WshClient, data: ActivityUpdate, opts?: RpcOpts): Promise<void> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "activity", data, opts);
|
||||
return client.wshRpcCall("activity", data, opts);
|
||||
}
|
||||
|
||||
// command "aisendmessage" [call]
|
||||
AiSendMessageCommand(client: WshClient, data: AiMessageData, opts?: RpcOpts): Promise<void> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "aisendmessage", data, opts);
|
||||
|
|
@ -612,12 +606,6 @@ export class RpcApiType {
|
|||
return client.wshRpcCall("path", data, opts);
|
||||
}
|
||||
|
||||
// command "recordtevent" [call]
|
||||
RecordTEventCommand(client: WshClient, data: TEvent, opts?: RpcOpts): Promise<void> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "recordtevent", data, opts);
|
||||
return client.wshRpcCall("recordtevent", data, opts);
|
||||
}
|
||||
|
||||
// command "remotedisconnectfromjobmanager" [call]
|
||||
RemoteDisconnectFromJobManagerCommand(client: WshClient, data: CommandRemoteDisconnectFromJobManagerData, opts?: RpcOpts): Promise<void> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "remotedisconnectfromjobmanager", data, opts);
|
||||
|
|
@ -756,12 +744,6 @@ export class RpcApiType {
|
|||
return client.wshRpcCall("routeunannounce", null, opts);
|
||||
}
|
||||
|
||||
// command "sendtelemetry" [call]
|
||||
SendTelemetryCommand(client: WshClient, opts?: RpcOpts): Promise<void> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "sendtelemetry", null, opts);
|
||||
return client.wshRpcCall("sendtelemetry", null, opts);
|
||||
}
|
||||
|
||||
// command "setblockfocus" [call]
|
||||
SetBlockFocusCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "setblockfocus", data, opts);
|
||||
|
|
@ -906,12 +888,6 @@ export class RpcApiType {
|
|||
return client.wshRpcCall("waveaiaddcontext", data, opts);
|
||||
}
|
||||
|
||||
// command "waveaienabletelemetry" [call]
|
||||
WaveAIEnableTelemetryCommand(client: WshClient, opts?: RpcOpts): Promise<void> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "waveaienabletelemetry", null, opts);
|
||||
return client.wshRpcCall("waveaienabletelemetry", null, opts);
|
||||
}
|
||||
|
||||
// command "waveaigettooldiff" [call]
|
||||
WaveAIGetToolDiffCommand(client: WshClient, data: CommandWaveAIGetToolDiffData, opts?: RpcOpts): Promise<CommandWaveAIGetToolDiffRtnData> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "waveaigettooldiff", data, opts);
|
||||
|
|
@ -954,12 +930,6 @@ export class RpcApiType {
|
|||
return client.wshRpcCall("writetempfile", data, opts);
|
||||
}
|
||||
|
||||
// command "wshactivity" [call]
|
||||
WshActivityCommand(client: WshClient, data: {[key: string]: number}, opts?: RpcOpts): Promise<void> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "wshactivity", data, opts);
|
||||
return client.wshRpcCall("wshactivity", data, opts);
|
||||
}
|
||||
|
||||
// command "wsldefaultdistro" [call]
|
||||
WslDefaultDistroCommand(client: WshClient, opts?: RpcOpts): Promise<string> {
|
||||
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "wsldefaultdistro", null, opts);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import { buildTabContextMenu } from "./tabcontextmenu";
|
|||
|
||||
export type TabEnv = WaveEnvSubset<{
|
||||
rpc: {
|
||||
ActivityCommand: WaveEnv["rpc"]["ActivityCommand"];
|
||||
SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"];
|
||||
SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"];
|
||||
UpdateTabNameCommand: WaveEnv["rpc"]["UpdateTabNameCommand"];
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ export type TabBarEnv = WaveEnvSubset<{
|
|||
installAppUpdate: WaveEnv["electron"]["installAppUpdate"];
|
||||
};
|
||||
rpc: {
|
||||
ActivityCommand: WaveEnv["rpc"]["ActivityCommand"];
|
||||
SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"];
|
||||
SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"];
|
||||
UpdateTabNameCommand: WaveEnv["rpc"]["UpdateTabNameCommand"];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2026, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { getOrefMetaKeyAtom, globalStore, recordTEvent } from "@/app/store/global";
|
||||
import { getOrefMetaKeyAtom, globalStore } from "@/app/store/global";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { fireAndForget } from "@/util/util";
|
||||
import { makeORef } from "../store/wos";
|
||||
|
|
@ -93,8 +93,6 @@ export function buildTabContextMenu(
|
|||
oref,
|
||||
meta: { "bg:*": true, "tab:background": null },
|
||||
});
|
||||
env.rpc.ActivityCommand(TabRpcClient, { settabtheme: 1 }, { noresponse: true });
|
||||
recordTEvent("action:settabtheme");
|
||||
}),
|
||||
});
|
||||
for (const bgKey of bgKeys) {
|
||||
|
|
@ -107,8 +105,6 @@ export function buildTabContextMenu(
|
|||
oref,
|
||||
meta: { "bg:*": true, "tab:background": bgKey },
|
||||
});
|
||||
env.rpc.ActivityCommand(TabRpcClient, { settabtheme: 1 }, { noresponse: true });
|
||||
recordTEvent("action:settabtheme");
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ export type VTabBarEnv = WaveEnvSubset<{
|
|||
rpc: {
|
||||
UpdateWorkspaceTabIdsCommand: WaveEnv["rpc"]["UpdateWorkspaceTabIdsCommand"];
|
||||
UpdateTabNameCommand: WaveEnv["rpc"]["UpdateTabNameCommand"];
|
||||
ActivityCommand: WaveEnv["rpc"]["ActivityCommand"];
|
||||
SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"];
|
||||
SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
getBlockTermDurableAtom,
|
||||
getOverrideConfigAtom,
|
||||
globalStore,
|
||||
recordTEvent,
|
||||
WOS,
|
||||
} from "@/store/global";
|
||||
import { base64ToString, fireAndForget, isSshConnName, isWslConnName } from "@/util/util";
|
||||
|
|
@ -52,42 +51,6 @@ function normalizeCmd(decodedCmd: string): string {
|
|||
return normalizedCmd;
|
||||
}
|
||||
|
||||
function checkCommandForTelemetry(decodedCmd: string) {
|
||||
if (!decodedCmd) {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedCmd = normalizeCmd(decodedCmd);
|
||||
|
||||
if (normalizedCmd.startsWith("ssh ")) {
|
||||
recordTEvent("conn:connect", { "conn:conntype": "ssh-manual" });
|
||||
return;
|
||||
}
|
||||
|
||||
const editorsRegex = /^(vim|vi|nano|nvim)\b/;
|
||||
if (editorsRegex.test(normalizedCmd)) {
|
||||
recordTEvent("action:term", { "action:type": "cli-edit" });
|
||||
return;
|
||||
}
|
||||
|
||||
const tailFollowRegex = /(^|\|\s*)tail\s+-[fF]\b/;
|
||||
if (tailFollowRegex.test(normalizedCmd)) {
|
||||
recordTEvent("action:term", { "action:type": "cli-tailf" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (ClaudeCodeRegex.test(normalizedCmd)) {
|
||||
recordTEvent("action:term", { "action:type": "claude" });
|
||||
return;
|
||||
}
|
||||
|
||||
const opencodeRegex = /^opencode\b/;
|
||||
if (opencodeRegex.test(normalizedCmd)) {
|
||||
recordTEvent("action:term", { "action:type": "opencode" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export function isClaudeCodeCommand(decodedCmd: string): boolean {
|
||||
if (!decodedCmd) {
|
||||
return false;
|
||||
|
|
@ -107,7 +70,6 @@ function handleShellIntegrationCommandStart(
|
|||
const isRemote = isSshConnName(connName);
|
||||
const isWsl = isWslConnName(connName);
|
||||
const isDurable = globalStore.get(getBlockTermDurableAtom(blockId)) ?? false;
|
||||
getApi().incrementTermCommands({ isRemote, isWsl, isDurable });
|
||||
if (cmd.data.cmd64) {
|
||||
const decodedLen = Math.ceil(cmd.data.cmd64.length * 0.75);
|
||||
if (decodedLen > 8192) {
|
||||
|
|
@ -120,7 +82,6 @@ function handleShellIntegrationCommandStart(
|
|||
globalStore.set(termWrap.lastCommandAtom, decodedCmd);
|
||||
const isCC = isClaudeCodeCommand(decodedCmd);
|
||||
globalStore.set(termWrap.claudeCodeActiveAtom, isCC);
|
||||
checkCommandForTelemetry(decodedCmd);
|
||||
} catch (e) {
|
||||
console.error("Error decoding cmd64:", e);
|
||||
rtInfo["shell:lastcmd"] = null;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import {
|
|||
getSettingsKeyAtom,
|
||||
globalStore,
|
||||
readAtom,
|
||||
recordTEvent,
|
||||
useBlockAtom,
|
||||
WOS,
|
||||
} from "@/store/global";
|
||||
|
|
@ -617,9 +616,8 @@ export class TermViewModel implements ViewModel {
|
|||
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:r")) {
|
||||
const shellIntegrationStatus = readAtom(this.termRef?.current?.shellIntegrationStatusAtom);
|
||||
if (shellIntegrationStatus === "ready") {
|
||||
recordTEvent("action:term", { "action:type": "term:ctrlr" });
|
||||
// allow this keybinding through, back to the terminal
|
||||
}
|
||||
// just for telemetry, we allow this keybinding through, back to the terminal
|
||||
return false;
|
||||
}
|
||||
if (keyutil.checkKeyPressed(waveEvent, "Cmd:Escape")) {
|
||||
|
|
|
|||
|
|
@ -489,16 +489,6 @@ export class WaveConfigViewModel implements ViewModel {
|
|||
|
||||
try {
|
||||
await this.env.rpc.SetSecretsCommand(TabRpcClient, { [selectedSecret]: secretValue });
|
||||
this.env.rpc.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "action:other",
|
||||
props: {
|
||||
"action:type": "waveconfig:savesecret",
|
||||
},
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
this.closeSecretView();
|
||||
} catch (error) {
|
||||
globalStore.set(this.errorMessageAtom, `Failed to save secret: ${error.message}`);
|
||||
|
|
@ -570,16 +560,6 @@ export class WaveConfigViewModel implements ViewModel {
|
|||
|
||||
try {
|
||||
await this.env.rpc.SetSecretsCommand(TabRpcClient, { [name]: value });
|
||||
this.env.rpc.RecordTEventCommand(
|
||||
TabRpcClient,
|
||||
{
|
||||
event: "action:other",
|
||||
props: {
|
||||
"action:type": "waveconfig:savesecret",
|
||||
},
|
||||
},
|
||||
{ noresponse: true }
|
||||
);
|
||||
globalStore.set(this.isAddingNewAtom, false);
|
||||
globalStore.set(this.newSecretNameAtom, "");
|
||||
globalStore.set(this.newSecretValueAtom, "");
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export type WaveConfigEnv = WaveEnvSubset<{
|
|||
GetSecretsNamesCommand: WaveEnv["rpc"]["GetSecretsNamesCommand"];
|
||||
GetSecretsCommand: WaveEnv["rpc"]["GetSecretsCommand"];
|
||||
SetSecretsCommand: WaveEnv["rpc"]["SetSecretsCommand"];
|
||||
RecordTEventCommand: WaveEnv["rpc"]["RecordTEventCommand"];
|
||||
};
|
||||
atoms: {
|
||||
fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"];
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import * as WOS from "@/app/store/wos";
|
|||
import { RpcApi } from "@/app/store/wshclientapi";
|
||||
import { TabRpcClient } from "@/app/store/wshrpcutil";
|
||||
import { getLayoutModelForStaticTab } from "@/layout/lib/layoutModelHooks";
|
||||
import { atoms, getApi, getOrefMetaKeyAtom, getSettingsKeyAtom, recordTEvent, refocusNode } from "@/store/global";
|
||||
import { atoms, getApi, getOrefMetaKeyAtom, getSettingsKeyAtom, refocusNode } from "@/store/global";
|
||||
import debug from "debug";
|
||||
import * as jotai from "jotai";
|
||||
import { debounce } from "lodash-es";
|
||||
|
|
@ -391,9 +391,6 @@ class WorkspaceLayoutModel {
|
|||
}
|
||||
const wasVisible = this.aiPanelVisible;
|
||||
this.aiPanelVisible = visible;
|
||||
if (visible && !wasVisible) {
|
||||
recordTEvent("action:openwaveai");
|
||||
}
|
||||
globalStore.set(this.panelVisibleAtom, visible);
|
||||
getApi().setWaveAIOpen(visible);
|
||||
RpcApi.SetMetaCommand(TabRpcClient, {
|
||||
|
|
|
|||
|
|
@ -314,7 +314,6 @@ function createMockFilesystemEntries(): MockFsEntryInput[] {
|
|||
),
|
||||
makeMockFsInput(`${MockHomePath}/.config/preview-cache.json`),
|
||||
makeMockFsInput(`${MockHomePath}/.config/recent-workspaces.json`),
|
||||
makeMockFsInput(`${MockHomePath}/.config/telemetry.log`),
|
||||
];
|
||||
return entries;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,11 +45,9 @@ const previewElectronApi: ElectronApi = {
|
|||
captureScreenshot: (_rect: Electron.Rectangle) => Promise.resolve(""),
|
||||
setKeyboardChordMode: () => {},
|
||||
setWaveAIOpen: (_isOpen: boolean) => {},
|
||||
incrementTermCommands: (_opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) => {},
|
||||
nativePaste: () => {},
|
||||
doRefresh: () => {},
|
||||
saveTextFile: (_fileName: string, _content: string) => Promise.resolve(false),
|
||||
setIsActive: async () => {},
|
||||
getPathForFile: (_file: File) => "",
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import Logo from "@/app/asset/logo.svg";
|
||||
import { InitPage, NoTelemetryStarPage } from "@/app/onboarding/onboarding";
|
||||
import { InitPage } from "@/app/onboarding/onboarding";
|
||||
import { OnboardingGradientBg } from "@/app/onboarding/onboarding-common";
|
||||
import { DurableSessionPage } from "@/app/onboarding/onboarding-durable";
|
||||
import { FilesPage, MagnifyBlocksPage, WaveAIPage } from "@/app/onboarding/onboarding-features";
|
||||
|
|
@ -24,10 +24,7 @@ function OnboardingFeaturesV() {
|
|||
return (
|
||||
<div className="flex flex-col w-full gap-8">
|
||||
<OnboardingModalWrapper width="w-[560px]">
|
||||
<InitPage isCompact={false} telemetryUpdateFn={async () => {}} />
|
||||
</OnboardingModalWrapper>
|
||||
<OnboardingModalWrapper width="w-[560px]">
|
||||
<NoTelemetryStarPage isCompact={false} />
|
||||
<InitPage isCompact={false} />
|
||||
</OnboardingModalWrapper>
|
||||
<OnboardingModalWrapper width="w-[800px]">
|
||||
<WaveAIPage onNext={noop} onSkip={noop} />
|
||||
|
|
|
|||
2
frontend/types/custom.d.ts
vendored
2
frontend/types/custom.d.ts
vendored
|
|
@ -113,12 +113,10 @@ declare global {
|
|||
captureScreenshot(rect: Electron.Rectangle): Promise<string>; // capture-screenshot
|
||||
setKeyboardChordMode: () => void; // set-keyboard-chord-mode
|
||||
setWaveAIOpen: (isOpen: boolean) => void; // set-waveai-open
|
||||
incrementTermCommands: (opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) => void; // increment-term-commands
|
||||
nativePaste: () => void; // native-paste
|
||||
doRefresh: () => void; // do-refresh
|
||||
getPathForFile: (file: File) => string; // webUtils.getPathForFile
|
||||
saveTextFile: (fileName: string, content: string) => Promise<boolean>; // save-text-file
|
||||
setIsActive: () => Promise<void>; // set-is-active
|
||||
};
|
||||
|
||||
type ElectronContextMenuItem = {
|
||||
|
|
|
|||
176
frontend/types/gotypes.d.ts
vendored
176
frontend/types/gotypes.d.ts
vendored
|
|
@ -70,44 +70,6 @@ declare global {
|
|||
configs: {[key: string]: AIModelConfigType};
|
||||
};
|
||||
|
||||
// wshrpc.ActivityDisplayType
|
||||
type ActivityDisplayType = {
|
||||
width: number;
|
||||
height: number;
|
||||
dpr: number;
|
||||
internal?: boolean;
|
||||
};
|
||||
|
||||
// wshrpc.ActivityUpdate
|
||||
type ActivityUpdate = {
|
||||
fgminutes?: number;
|
||||
activeminutes?: number;
|
||||
openminutes?: number;
|
||||
waveaifgminutes?: number;
|
||||
waveaiactiveminutes?: number;
|
||||
numtabs?: number;
|
||||
newtab?: number;
|
||||
numblocks?: number;
|
||||
numwindows?: number;
|
||||
numws?: number;
|
||||
numwsnamed?: number;
|
||||
numsshconn?: number;
|
||||
numwslconn?: number;
|
||||
nummagnify?: number;
|
||||
termcommandsrun?: number;
|
||||
numpanics?: number;
|
||||
numaireqs?: number;
|
||||
startup?: number;
|
||||
shutdown?: number;
|
||||
settabtheme?: number;
|
||||
buildtime?: string;
|
||||
displays?: ActivityDisplayType[];
|
||||
renderers?: {[key: string]: number};
|
||||
blocks?: {[key: string]: number};
|
||||
wshcmds?: {[key: string]: number};
|
||||
conn?: {[key: string]: number};
|
||||
};
|
||||
|
||||
// wshrpc.AiMessageData
|
||||
type AiMessageData = {
|
||||
message?: string;
|
||||
|
|
@ -1320,8 +1282,6 @@ declare global {
|
|||
"window:savelastwindow"?: boolean;
|
||||
"window:dimensions"?: string;
|
||||
"window:zoom"?: number;
|
||||
"telemetry:*"?: boolean;
|
||||
"telemetry:enabled"?: boolean;
|
||||
"conn:*"?: boolean;
|
||||
"conn:askbeforewshinstall"?: boolean;
|
||||
"conn:wshenabled"?: boolean;
|
||||
|
|
@ -1386,142 +1346,6 @@ declare global {
|
|||
"url:url"?: string;
|
||||
};
|
||||
|
||||
// telemetrydata.TEvent
|
||||
type TEvent = {
|
||||
uuid?: string;
|
||||
ts?: number;
|
||||
tslocal?: string;
|
||||
event: string;
|
||||
props: TEventProps;
|
||||
};
|
||||
|
||||
// telemetrydata.TEventProps
|
||||
type TEventProps = {
|
||||
"client:arch"?: string;
|
||||
"client:version"?: string;
|
||||
"client:initial_version"?: string;
|
||||
"client:buildtime"?: string;
|
||||
"client:osrelease"?: string;
|
||||
"client:isdev"?: boolean;
|
||||
"client:packagetype"?: string;
|
||||
"client:macos"?: string;
|
||||
"cohort:month"?: string;
|
||||
"cohort:isoweek"?: string;
|
||||
"autoupdate:channel"?: string;
|
||||
"autoupdate:enabled"?: boolean;
|
||||
"localshell:type"?: string;
|
||||
"localshell:version"?: string;
|
||||
"loc:countrycode"?: string;
|
||||
"loc:regioncode"?: string;
|
||||
"settings:customwidgets"?: number;
|
||||
"settings:customaipresets"?: number;
|
||||
"settings:customsettings"?: number;
|
||||
"settings:customaimodes"?: number;
|
||||
"settings:secretscount"?: number;
|
||||
"settings:transparent"?: boolean;
|
||||
"activity:activeminutes"?: number;
|
||||
"activity:fgminutes"?: number;
|
||||
"activity:openminutes"?: number;
|
||||
"activity:waveaiactiveminutes"?: number;
|
||||
"activity:waveaifgminutes"?: number;
|
||||
"activity:termcommandsrun"?: number;
|
||||
"activity:termcommands:remote"?: number;
|
||||
"activity:termcommands:durable"?: number;
|
||||
"activity:termcommands:wsl"?: number;
|
||||
"app:firstday"?: boolean;
|
||||
"app:firstlaunch"?: boolean;
|
||||
"action:initiator"?: "keyboard" | "mouse";
|
||||
"action:type"?: string;
|
||||
"debug:panictype"?: string;
|
||||
"block:view"?: string;
|
||||
"block:controller"?: string;
|
||||
"block:subblock"?: boolean;
|
||||
"ai:backendtype"?: string;
|
||||
"ai:local"?: boolean;
|
||||
"wsh:cmd"?: string;
|
||||
"wsh:errorcount"?: number;
|
||||
"wsh:count"?: number;
|
||||
"conn:conntype"?: string;
|
||||
"conn:wsherrorcode"?: string;
|
||||
"conn:errorcode"?: string;
|
||||
"conn:suberrorcode"?: string;
|
||||
"conn:contexterror"?: boolean;
|
||||
"onboarding:feature"?: "waveai" | "durable" | "magnify" | "wsh";
|
||||
"onboarding:version"?: string;
|
||||
"onboarding:githubstar"?: "already" | "star" | "later";
|
||||
"onboarding:page"?: string;
|
||||
"display:height"?: number;
|
||||
"display:width"?: number;
|
||||
"display:dpr"?: number;
|
||||
"display:count"?: number;
|
||||
"display:all"?: any;
|
||||
"count:blocks"?: number;
|
||||
"count:tabs"?: number;
|
||||
"count:windows"?: number;
|
||||
"count:workspaces"?: number;
|
||||
"count:sshconn"?: number;
|
||||
"count:wslconn"?: number;
|
||||
"count:jobs"?: number;
|
||||
"count:jobsconnected"?: number;
|
||||
"count:views"?: {[key: string]: number};
|
||||
"waveai:apitype"?: string;
|
||||
"waveai:model"?: string;
|
||||
"waveai:chatid"?: string;
|
||||
"waveai:stepnum"?: number;
|
||||
"waveai:inputtokens"?: number;
|
||||
"waveai:outputtokens"?: number;
|
||||
"waveai:nativewebsearchcount"?: number;
|
||||
"waveai:requestcount"?: number;
|
||||
"waveai:toolusecount"?: number;
|
||||
"waveai:tooluseerrorcount"?: number;
|
||||
"waveai:tooldetail"?: {[key: string]: number};
|
||||
"waveai:proxyreq"?: number;
|
||||
"waveai:haderror"?: boolean;
|
||||
"waveai:imagecount"?: number;
|
||||
"waveai:pdfcount"?: number;
|
||||
"waveai:textdoccount"?: number;
|
||||
"waveai:textlen"?: number;
|
||||
"waveai:firstbytems"?: number;
|
||||
"waveai:requestdurms"?: number;
|
||||
"waveai:widgetaccess"?: boolean;
|
||||
"waveai:thinkinglevel"?: string;
|
||||
"waveai:mode"?: string;
|
||||
"waveai:provider"?: string;
|
||||
"waveai:islocal"?: boolean;
|
||||
"waveai:feedback"?: "good" | "bad";
|
||||
"waveai:action"?: string;
|
||||
"job:donereason"?: string;
|
||||
"job:kind"?: string;
|
||||
$set?: TEventUserProps;
|
||||
$set_once?: TEventUserProps;
|
||||
};
|
||||
|
||||
// telemetrydata.TEventUserProps
|
||||
type TEventUserProps = {
|
||||
"client:arch"?: string;
|
||||
"client:version"?: string;
|
||||
"client:initial_version"?: string;
|
||||
"client:buildtime"?: string;
|
||||
"client:osrelease"?: string;
|
||||
"client:isdev"?: boolean;
|
||||
"client:packagetype"?: string;
|
||||
"client:macos"?: string;
|
||||
"cohort:month"?: string;
|
||||
"cohort:isoweek"?: string;
|
||||
"autoupdate:channel"?: string;
|
||||
"autoupdate:enabled"?: boolean;
|
||||
"localshell:type"?: string;
|
||||
"localshell:version"?: string;
|
||||
"loc:countrycode"?: string;
|
||||
"loc:regioncode"?: string;
|
||||
"settings:customwidgets"?: number;
|
||||
"settings:customaipresets"?: number;
|
||||
"settings:customsettings"?: number;
|
||||
"settings:customaimodes"?: number;
|
||||
"settings:secretscount"?: number;
|
||||
"settings:transparent"?: boolean;
|
||||
};
|
||||
|
||||
// waveobj.Tab
|
||||
type Tab = WaveObj & {
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ type ToolDefinition struct {
|
|||
DisplayName string `json:"displayname,omitempty"` // internal field (cannot marshal to API, must be stripped)
|
||||
Description string `json:"description"`
|
||||
ShortDescription string `json:"shortdescription,omitempty"` // internal field (cannot marshal to API, must be stripped)
|
||||
ToolLogName string `json:"-"` // short name for telemetry (e.g., "term:getscrollback")
|
||||
ToolLogName string `json:"-"` // short name for logging (e.g., "term:getscrollback")
|
||||
InputSchema map[string]any `json:"input_schema"`
|
||||
Strict bool `json:"strict,omitempty"`
|
||||
RequiredCapabilities []string `json:"requiredcapabilities,omitempty"`
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ import (
|
|||
"github.com/wavetermdev/waveterm/pkg/aiusechat/chatstore"
|
||||
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
|
||||
"github.com/wavetermdev/waveterm/pkg/secretstore"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/ds"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/logutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
|
|
@ -111,9 +109,6 @@ func getWaveAISettings(rtInfo waveobj.ObjRTInfo, aiModeName string, aiModelName
|
|||
} else if len(modelConfig.Capabilities) > 0 {
|
||||
config.Capabilities = modelConfig.Capabilities
|
||||
}
|
||||
if config.WaveAICloud && !telemetry.IsTelemetryEnabled() {
|
||||
return nil, fmt.Errorf("Wave AI cloud modes require telemetry to be enabled")
|
||||
}
|
||||
apiToken := config.APIToken
|
||||
if apiToken == "" && config.APITokenSecretName != "" {
|
||||
secret, exists, err := secretstore.GetSecret(config.APITokenSecretName)
|
||||
|
|
@ -578,42 +573,10 @@ func WaveAIPostMessageWrap(ctx context.Context, sseHandler *sse.SSEHandlerCh, me
|
|||
log.Printf("WaveAI call metrics: requests=%d tools=%d proxy=%d images=%d pdfs=%d textdocs=%d textlen=%d duration=%dms error=%v\n",
|
||||
metrics.RequestCount, metrics.ToolUseCount, metrics.ProxyReqCount,
|
||||
metrics.ImageCount, metrics.PDFCount, metrics.TextDocCount, metrics.TextLen, metrics.RequestDuration, metrics.HadError)
|
||||
|
||||
sendAIMetricsTelemetry(ctx, metrics)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func sendAIMetricsTelemetry(ctx context.Context, metrics *uctypes.AIMetrics) {
|
||||
event := telemetrydata.MakeTEvent("waveai:post", telemetrydata.TEventProps{
|
||||
WaveAIAPIType: metrics.Usage.APIType,
|
||||
WaveAIModel: metrics.Usage.Model,
|
||||
WaveAIChatId: metrics.ChatId,
|
||||
WaveAIStepNum: metrics.StepNum,
|
||||
WaveAIInputTokens: metrics.Usage.InputTokens,
|
||||
WaveAIOutputTokens: metrics.Usage.OutputTokens,
|
||||
WaveAINativeWebSearchCount: metrics.Usage.NativeWebSearchCount,
|
||||
WaveAIRequestCount: metrics.RequestCount,
|
||||
WaveAIToolUseCount: metrics.ToolUseCount,
|
||||
WaveAIToolUseErrorCount: metrics.ToolUseErrorCount,
|
||||
WaveAIToolDetail: metrics.ToolDetail,
|
||||
WaveAIProxyReq: metrics.ProxyReqCount,
|
||||
WaveAIHadError: metrics.HadError,
|
||||
WaveAIImageCount: metrics.ImageCount,
|
||||
WaveAIPDFCount: metrics.PDFCount,
|
||||
WaveAITextDocCount: metrics.TextDocCount,
|
||||
WaveAITextLen: metrics.TextLen,
|
||||
WaveAIFirstByteMs: metrics.FirstByteLatency,
|
||||
WaveAIRequestDurMs: metrics.RequestDuration,
|
||||
WaveAIWidgetAccess: metrics.WidgetAccess,
|
||||
WaveAIThinkingLevel: metrics.ThinkingLevel,
|
||||
WaveAIMode: metrics.AIMode,
|
||||
WaveAIProvider: metrics.AIProvider,
|
||||
WaveAIIsLocal: metrics.IsLocal,
|
||||
})
|
||||
_ = telemetry.RecordTEvent(ctx, event)
|
||||
}
|
||||
|
||||
// PostMessageRequest represents the request body for posting a message
|
||||
type PostMessageRequest struct {
|
||||
TabId string `json:"tabid,omitempty"`
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ import (
|
|||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
|
||||
"github.com/wavetermdev/waveterm/pkg/streamclient"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/ds"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/envutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
|
||||
|
|
@ -727,13 +725,6 @@ func StartJob(ctx context.Context, params StartJobParams) (string, error) {
|
|||
updatedJob = job
|
||||
})
|
||||
sendBlockJobStatusEventByJob(ctx, updatedJob)
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "job:done",
|
||||
Props: telemetrydata.TEventProps{
|
||||
JobDoneReason: JobDoneReason_StartupError,
|
||||
JobKind: params.JobKind,
|
||||
},
|
||||
})
|
||||
return "", fmt.Errorf("failed to start remote job: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -754,13 +745,6 @@ func StartJob(ctx context.Context, params StartJobParams) (string, error) {
|
|||
sendBlockJobStatusEventByJob(ctx, updatedJob)
|
||||
}
|
||||
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "job:start",
|
||||
Props: telemetrydata.TEventProps{
|
||||
JobKind: params.JobKind,
|
||||
},
|
||||
})
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("jobcontroller:runOutputLoop", recover())
|
||||
|
|
@ -1003,6 +987,7 @@ func remoteTerminateJobManager(ctx context.Context, job *waveobj.Job) error {
|
|||
}
|
||||
|
||||
bareRpc := wshclient.GetBareRpcClient()
|
||||
|
||||
terminateData := wshrpc.CommandRemoteTerminateJobManagerData{
|
||||
JobId: job.OID,
|
||||
JobManagerPid: job.JobManagerPid,
|
||||
|
|
@ -1037,14 +1022,6 @@ func remoteTerminateJobManager(ctx context.Context, job *waveobj.Job) error {
|
|||
sendBlockJobStatusEventByJob(ctx, updatedJob)
|
||||
}
|
||||
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "job:done",
|
||||
Props: telemetrydata.TEventProps{
|
||||
JobDoneReason: JobDoneReason_Terminated,
|
||||
JobKind: job.JobKind,
|
||||
},
|
||||
})
|
||||
|
||||
log.Printf("[job:%s] job manager terminated successfully", job.OID)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1132,13 +1109,6 @@ func doReconnectJob(ctx context.Context, jobId string, rtOpts *waveobj.RuntimeOp
|
|||
} else {
|
||||
sendBlockJobStatusEventByJob(ctx, updatedJob)
|
||||
}
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "job:done",
|
||||
Props: telemetrydata.TEventProps{
|
||||
JobDoneReason: JobDoneReason_Gone,
|
||||
JobKind: job.JobKind,
|
||||
},
|
||||
})
|
||||
writeJobTerminationMessage(ctx, jobId, updatedJob, "[session gone]")
|
||||
return fmt.Errorf("job manager has exited: %s", rtnData.Error)
|
||||
}
|
||||
|
|
@ -1157,13 +1127,6 @@ func doReconnectJob(ctx context.Context, jobId string, rtOpts *waveobj.RuntimeOp
|
|||
SetJobConnStatus(jobId, JobConnStatus_Connected)
|
||||
sendBlockJobStatusEventByJob(ctx, job)
|
||||
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "job:reconnect",
|
||||
Props: telemetrydata.TEventProps{
|
||||
JobKind: job.JobKind,
|
||||
},
|
||||
})
|
||||
|
||||
log.Printf("[job:%s] route established, restarting streaming", jobId)
|
||||
return restartStreaming(ctx, jobId, true, rtOpts)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,6 @@ import (
|
|||
"runtime/debug"
|
||||
)
|
||||
|
||||
// to log NumPanics into the local telemetry system
|
||||
// gets around import cycles
|
||||
var PanicTelemetryHandler func(panicType string)
|
||||
|
||||
func PanicHandlerNoTelemetry(debugStr string, recoverVal any) {
|
||||
if recoverVal == nil {
|
||||
return
|
||||
|
|
@ -28,14 +24,6 @@ func PanicHandler(debugStr string, recoverVal any) error {
|
|||
}
|
||||
log.Printf("[panic] in %s: %v\n", debugStr, recoverVal)
|
||||
debug.PrintStack()
|
||||
if PanicTelemetryHandler != nil {
|
||||
go func() {
|
||||
defer func() {
|
||||
PanicHandlerNoTelemetry("PanicTelemetryHandler", recover())
|
||||
}()
|
||||
PanicTelemetryHandler(debugStr)
|
||||
}()
|
||||
}
|
||||
if err, ok := recoverVal.(error); ok {
|
||||
return fmt.Errorf("panic in %s: %w", debugStr, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ import (
|
|||
"github.com/wavetermdev/waveterm/pkg/genconn"
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/remote"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/userinput"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/envutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
|
||||
|
|
@ -749,26 +747,13 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wconfig.ConnKeyword
|
|||
conn.FireConnChangeEvent()
|
||||
err := conn.connectInternal(ctx, connFlags)
|
||||
if err != nil {
|
||||
errorCode, subCode := remote.ClassifyConnError(err)
|
||||
isContextError := errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
|
||||
errorCode, _ := remote.ClassifyConnError(err)
|
||||
conn.Infof(ctx, "ERROR [%s] %v\n\n", errorCode, err)
|
||||
conn.WithLock(func() {
|
||||
conn.Status = Status_Error
|
||||
conn.Error = err.Error()
|
||||
})
|
||||
conn.closeInternal_withlifecyclelock()
|
||||
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
|
||||
Conn: map[string]int{"ssh:connecterror": 1},
|
||||
}, "ssh-connconnect")
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "conn:connecterror",
|
||||
Props: telemetrydata.TEventProps{
|
||||
ConnType: "ssh",
|
||||
ConnErrorCode: errorCode,
|
||||
ConnSubErrorCode: subCode,
|
||||
ConnContextError: isContextError,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
conn.Infof(ctx, "successfully connected (wsh:%v)\n\n", conn.WshEnabled.Load())
|
||||
conn.WithLock(func() {
|
||||
|
|
@ -778,15 +763,6 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wconfig.ConnKeyword
|
|||
conn.ActiveConnNum = int(activeConnCounter.Add(1))
|
||||
}
|
||||
})
|
||||
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
|
||||
Conn: map[string]int{"ssh:connect": 1},
|
||||
}, "ssh-connconnect")
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "conn:connect",
|
||||
Props: telemetrydata.TEventProps{
|
||||
ConnType: "ssh",
|
||||
},
|
||||
})
|
||||
}
|
||||
conn.FireConnChangeEvent()
|
||||
if err != nil {
|
||||
|
|
@ -984,13 +960,6 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wconfig.Con
|
|||
} else {
|
||||
conn.Infof(ctx, "wsh not enabled: %s\n", wshResult.NoWshReason)
|
||||
}
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "conn:nowsh",
|
||||
Props: telemetrydata.TEventProps{
|
||||
ConnType: "ssh",
|
||||
ConnWshErrorCode: wshResult.NoWshCode,
|
||||
},
|
||||
})
|
||||
}
|
||||
conn.persistWshInstalled(ctx, wshResult)
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import (
|
|||
|
||||
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||
"github.com/wavetermdev/waveterm/pkg/wcore"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wslconn"
|
||||
|
|
@ -65,14 +64,3 @@ func (cs *ClientService) AgreeTos(ctx context.Context) (waveobj.UpdatesRtnType,
|
|||
wcore.BootstrapStarterLayout(ctx)
|
||||
return waveobj.ContextGetUpdatesRtn(ctx), nil
|
||||
}
|
||||
|
||||
func (cs *ClientService) TelemetryUpdate(ctx context.Context, telemetryEnabled bool) error {
|
||||
meta := waveobj.MetaMapType{
|
||||
wconfig.ConfigKey_TelemetryEnabled: telemetryEnabled,
|
||||
}
|
||||
err := wconfig.SetBaseConfigValue(meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting telemetry value: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,466 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/daystr"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/dbutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wstore"
|
||||
)
|
||||
|
||||
const MaxTzNameLen = 50
|
||||
const ActivityEventName = "app:activity"
|
||||
const WshRunEventName = "wsh:run"
|
||||
|
||||
var cachedTosAgreedTs atomic.Int64
|
||||
|
||||
func GetTosAgreedTs() int64 {
|
||||
cached := cachedTosAgreedTs.Load()
|
||||
if cached != 0 {
|
||||
return cached
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
|
||||
if err != nil || client == nil || client.TosAgreed == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
cachedTosAgreedTs.Store(client.TosAgreed)
|
||||
return client.TosAgreed
|
||||
}
|
||||
|
||||
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"`
|
||||
OSRelease string `json:"osrelease"`
|
||||
}
|
||||
|
||||
type TelemetryData struct {
|
||||
ActiveMinutes int `json:"activeminutes"`
|
||||
FgMinutes int `json:"fgminutes"`
|
||||
OpenMinutes int `json:"openminutes"`
|
||||
WaveAIActiveMinutes int `json:"waveaiactiveminutes,omitempty"`
|
||||
WaveAIFgMinutes int `json:"waveaifgminutes,omitempty"`
|
||||
NumTabs int `json:"numtabs"`
|
||||
NumBlocks int `json:"numblocks,omitempty"`
|
||||
NumWindows int `json:"numwindows,omitempty"`
|
||||
NumWS int `json:"numws,omitempty"`
|
||||
NumWSNamed int `json:"numwsnamed,omitempty"`
|
||||
NumSSHConn int `json:"numsshconn,omitempty"`
|
||||
NumWSLConn int `json:"numwslconn,omitempty"`
|
||||
NumMagnify int `json:"nummagnify,omitempty"`
|
||||
NewTab int `json:"newtab"`
|
||||
NumStartup int `json:"numstartup,omitempty"`
|
||||
NumShutdown int `json:"numshutdown,omitempty"`
|
||||
NumPanics int `json:"numpanics,omitempty"`
|
||||
NumAIReqs int `json:"numaireqs,omitempty"`
|
||||
SetTabTheme int `json:"settabtheme,omitempty"`
|
||||
Displays []wshrpc.ActivityDisplayType `json:"displays,omitempty"`
|
||||
Renderers map[string]int `json:"renderers,omitempty"`
|
||||
Blocks map[string]int `json:"blocks,omitempty"`
|
||||
WshCmds map[string]int `json:"wshcmds,omitempty"`
|
||||
Conn map[string]int `json:"conn,omitempty"`
|
||||
}
|
||||
|
||||
func (tdata TelemetryData) Value() (driver.Value, error) {
|
||||
return dbutil.QuickValueJson(tdata)
|
||||
}
|
||||
|
||||
func (tdata *TelemetryData) Scan(val interface{}) error {
|
||||
return dbutil.QuickScanJson(tdata, val)
|
||||
}
|
||||
|
||||
func IsTelemetryEnabled() bool {
|
||||
settings := wconfig.GetWatcher().GetFullConfig()
|
||||
return settings.Settings.TelemetryEnabled
|
||||
}
|
||||
|
||||
func IsAutoUpdateEnabled() bool {
|
||||
settings := wconfig.GetWatcher().GetFullConfig()
|
||||
return settings.Settings.AutoUpdateEnabled
|
||||
}
|
||||
|
||||
func AutoUpdateChannel() string {
|
||||
settings := wconfig.GetWatcher().GetFullConfig()
|
||||
return settings.Settings.AutoUpdateChannel
|
||||
}
|
||||
|
||||
// Wraps UpdateCurrentActivity, spawns goroutine, and logs errors
|
||||
func GoUpdateActivityWrap(update wshrpc.ActivityUpdate, debugStr string) {
|
||||
go func() {
|
||||
defer func() {
|
||||
panichandler.PanicHandlerNoTelemetry("GoUpdateActivityWrap", recover())
|
||||
}()
|
||||
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 insertTEvent(ctx context.Context, event *telemetrydata.TEvent) error {
|
||||
if event.Uuid == "" {
|
||||
return fmt.Errorf("cannot insert TEvent: uuid is empty")
|
||||
}
|
||||
if event.Ts == 0 {
|
||||
return fmt.Errorf("cannot insert TEvent: ts is 0")
|
||||
}
|
||||
if event.TsLocal == "" {
|
||||
return fmt.Errorf("cannot insert TEvent: tslocal is empty")
|
||||
}
|
||||
if event.Event == "" {
|
||||
return fmt.Errorf("cannot insert TEvent: event is empty")
|
||||
}
|
||||
return wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {
|
||||
query := `INSERT INTO db_tevent (uuid, ts, tslocal, event, props)
|
||||
VALUES (?, ?, ?, ?, ?)`
|
||||
tx.Exec(query, event.Uuid, event.Ts, event.TsLocal, event.Event, dbutil.QuickJson(event.Props))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// merges newActivity into curActivity, returns curActivity
|
||||
func mergeActivity(curActivity *telemetrydata.TEventProps, newActivity telemetrydata.TEventProps) {
|
||||
curActivity.ActiveMinutes += newActivity.ActiveMinutes
|
||||
curActivity.FgMinutes += newActivity.FgMinutes
|
||||
curActivity.OpenMinutes += newActivity.OpenMinutes
|
||||
curActivity.WaveAIActiveMinutes += newActivity.WaveAIActiveMinutes
|
||||
curActivity.WaveAIFgMinutes += newActivity.WaveAIFgMinutes
|
||||
curActivity.TermCommandsRun += newActivity.TermCommandsRun
|
||||
curActivity.TermCommandsRemote += newActivity.TermCommandsRemote
|
||||
curActivity.TermCommandsDurable += newActivity.TermCommandsDurable
|
||||
curActivity.TermCommandsWsl += newActivity.TermCommandsWsl
|
||||
if newActivity.AppFirstDay {
|
||||
curActivity.AppFirstDay = true
|
||||
}
|
||||
}
|
||||
|
||||
// ignores the timestamp in tevent, and uses the current time
|
||||
func updateActivityTEvent(ctx context.Context, tevent *telemetrydata.TEvent) error {
|
||||
eventTs := time.Now()
|
||||
// compute to 1-hour boundary, and round up to next 1-hour boundary
|
||||
eventTs = eventTs.Truncate(time.Hour).Add(time.Hour)
|
||||
|
||||
return wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {
|
||||
// find event that matches this timestamp with event name "app:activity"
|
||||
var hasRow bool
|
||||
var curActivity telemetrydata.TEventProps
|
||||
uuidStr := tx.GetString(`SELECT uuid FROM db_tevent WHERE ts = ? AND event = ?`, eventTs.UnixMilli(), ActivityEventName)
|
||||
if uuidStr != "" {
|
||||
hasRow = true
|
||||
rawProps := tx.GetString(`SELECT props FROM db_tevent WHERE uuid = ?`, uuidStr)
|
||||
err := json.Unmarshal([]byte(rawProps), &curActivity)
|
||||
if err != nil {
|
||||
// ignore, curActivity will just be 0
|
||||
log.Printf("error unmarshalling activity props: %v\n", err)
|
||||
}
|
||||
}
|
||||
mergeActivity(&curActivity, tevent.Props)
|
||||
|
||||
if hasRow {
|
||||
query := `UPDATE db_tevent SET props = ? WHERE uuid = ?`
|
||||
tx.Exec(query, dbutil.QuickJson(curActivity), uuidStr)
|
||||
} else {
|
||||
query := `INSERT INTO db_tevent (uuid, ts, tslocal, event, props) VALUES (?, ?, ?, ?, ?)`
|
||||
tsLocal := utilfn.ConvertToWallClockPT(eventTs).Format(time.RFC3339)
|
||||
tx.Exec(query, uuid.New().String(), eventTs.UnixMilli(), tsLocal, ActivityEventName, dbutil.QuickJson(curActivity))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// aggregates wsh:run events per (cmd, haderror) key within the current 1-hour bucket
|
||||
func updateWshRunTEvent(ctx context.Context, tevent *telemetrydata.TEvent) error {
|
||||
eventTs := time.Now().Truncate(time.Hour).Add(time.Hour)
|
||||
incomingCount := tevent.Props.WshCount
|
||||
if incomingCount <= 0 {
|
||||
incomingCount = 1
|
||||
}
|
||||
return wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {
|
||||
uuidStr := tx.GetString(
|
||||
`SELECT uuid FROM db_tevent WHERE ts = ? AND event = ? AND json_extract(props, '$."wsh:cmd"') IS ?`,
|
||||
eventTs.UnixMilli(), WshRunEventName, tevent.Props.WshCmd,
|
||||
)
|
||||
if uuidStr != "" {
|
||||
var curProps telemetrydata.TEventProps
|
||||
rawProps := tx.GetString(`SELECT props FROM db_tevent WHERE uuid = ?`, uuidStr)
|
||||
if rawProps != "" {
|
||||
if err := json.Unmarshal([]byte(rawProps), &curProps); err != nil {
|
||||
log.Printf("error unmarshalling wsh:run props: %v\n", err)
|
||||
}
|
||||
}
|
||||
curCount := curProps.WshCount
|
||||
if curCount <= 0 {
|
||||
curCount = 1
|
||||
}
|
||||
curProps.WshCount = curCount + incomingCount
|
||||
curProps.WshErrorCount += tevent.Props.WshErrorCount
|
||||
tx.Exec(`UPDATE db_tevent SET props = ? WHERE uuid = ?`, dbutil.QuickJson(curProps), uuidStr)
|
||||
} else {
|
||||
newProps := tevent.Props
|
||||
newProps.WshCount = incomingCount
|
||||
tsLocal := utilfn.ConvertToWallClockPT(eventTs).Format(time.RFC3339)
|
||||
tx.Exec(`INSERT INTO db_tevent (uuid, ts, tslocal, event, props) VALUES (?, ?, ?, ?, ?)`,
|
||||
uuid.New().String(), eventTs.UnixMilli(), tsLocal, WshRunEventName, dbutil.QuickJson(newProps))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TruncateActivityTEventForShutdown(ctx context.Context) error {
|
||||
nowTs := time.Now()
|
||||
eventTs := nowTs.Truncate(time.Hour).Add(time.Hour)
|
||||
return wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {
|
||||
// find event that matches this timestamp with event name "app:activity"
|
||||
uuidStr := tx.GetString(`SELECT uuid FROM db_tevent WHERE ts = ? AND event = ?`, eventTs.UnixMilli(), ActivityEventName)
|
||||
if uuidStr == "" {
|
||||
return nil
|
||||
}
|
||||
// we're going to update this app:activity event back to nowTs
|
||||
tsLocal := utilfn.ConvertToWallClockPT(nowTs).Format(time.RFC3339)
|
||||
query := `UPDATE db_tevent SET ts = ?, tslocal = ? WHERE uuid = ?`
|
||||
tx.Exec(query, nowTs.UnixMilli(), tsLocal, uuidStr)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GoRecordTEventWrap(tevent *telemetrydata.TEvent) {
|
||||
if tevent == nil || tevent.Event == "" {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
panichandler.PanicHandlerNoTelemetry("GoRecordTEventWrap", recover())
|
||||
}()
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancelFn()
|
||||
err := RecordTEvent(ctx, tevent)
|
||||
if err != nil {
|
||||
// ignore error, just log, since this is not critical
|
||||
log.Printf("error recording %q telemetry event: %v\n", tevent.Event, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func RecordTEvent(ctx context.Context, tevent *telemetrydata.TEvent) error {
|
||||
if tevent == nil {
|
||||
return nil
|
||||
}
|
||||
if tevent.Uuid == "" {
|
||||
tevent.Uuid = uuid.New().String()
|
||||
}
|
||||
err := tevent.Validate(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tevent.EnsureTimestamps()
|
||||
|
||||
// Set AppFirstDay if on same calendar day as TOS agreement
|
||||
tosAgreedTs := GetTosAgreedTs()
|
||||
if tosAgreedTs == 0 {
|
||||
tevent.Props.AppFirstDay = true
|
||||
} else {
|
||||
tosYear, tosMonth, tosDay := time.UnixMilli(tosAgreedTs).Date()
|
||||
nowYear, nowMonth, nowDay := time.Now().Date()
|
||||
if tosYear == nowYear && tosMonth == nowMonth && tosDay == nowDay {
|
||||
tevent.Props.AppFirstDay = true
|
||||
}
|
||||
}
|
||||
|
||||
if tevent.Event == ActivityEventName {
|
||||
return updateActivityTEvent(ctx, tevent)
|
||||
}
|
||||
if tevent.Event == WshRunEventName {
|
||||
return updateWshRunTEvent(ctx, tevent)
|
||||
}
|
||||
return insertTEvent(ctx, tevent)
|
||||
}
|
||||
|
||||
func CleanOldTEvents(ctx context.Context) error {
|
||||
daysToKeep := 7
|
||||
if !IsTelemetryEnabled() {
|
||||
daysToKeep = 1
|
||||
}
|
||||
olderThan := time.Now().AddDate(0, 0, -daysToKeep).UnixMilli()
|
||||
return wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {
|
||||
query := `DELETE FROM db_tevent WHERE ts < ?`
|
||||
tx.Exec(query, olderThan)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetNonUploadedTEvents(ctx context.Context, maxEvents int) ([]*telemetrydata.TEvent, error) {
|
||||
now := time.Now()
|
||||
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) ([]*telemetrydata.TEvent, error) {
|
||||
var rtn []*telemetrydata.TEvent
|
||||
query := `SELECT uuid, ts, tslocal, event, props, uploaded FROM db_tevent WHERE uploaded = 0 AND ts <= ? ORDER BY ts LIMIT ?`
|
||||
tx.Select(&rtn, query, now.UnixMilli(), maxEvents)
|
||||
for _, event := range rtn {
|
||||
if err := event.ConvertRawJSON(); err != nil {
|
||||
return nil, fmt.Errorf("scan json for event %s: %w", event.Uuid, err)
|
||||
}
|
||||
}
|
||||
return rtn, nil
|
||||
})
|
||||
}
|
||||
|
||||
func MarkTEventsAsUploaded(ctx context.Context, events []*telemetrydata.TEvent) error {
|
||||
return wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {
|
||||
ids := make([]string, 0, len(events))
|
||||
for _, event := range events {
|
||||
ids = append(ids, event.Uuid)
|
||||
}
|
||||
query := `UPDATE db_tevent SET uploaded = 1 WHERE uuid IN (SELECT value FROM json_each(?))`
|
||||
tx.Exec(query, dbutil.QuickJson(ids))
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateActivity(ctx context.Context, update wshrpc.ActivityUpdate) error {
|
||||
now := time.Now()
|
||||
dayStr := daystr.GetCurDayStr()
|
||||
txErr := wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {
|
||||
var tdata TelemetryData
|
||||
query := `SELECT tdata FROM db_activity WHERE day = ?`
|
||||
found := tx.Get(&tdata, query, dayStr)
|
||||
if !found {
|
||||
query = `INSERT INTO db_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, wavebase.WaveVersion, wavebase.ClientArch(), wavebase.BuildTime, wavebase.UnameKernelRelease())
|
||||
}
|
||||
tdata.FgMinutes += update.FgMinutes
|
||||
tdata.ActiveMinutes += update.ActiveMinutes
|
||||
tdata.OpenMinutes += update.OpenMinutes
|
||||
tdata.WaveAIFgMinutes += update.WaveAIFgMinutes
|
||||
tdata.WaveAIActiveMinutes += update.WaveAIActiveMinutes
|
||||
tdata.NewTab += update.NewTab
|
||||
tdata.NumStartup += update.Startup
|
||||
tdata.NumShutdown += update.Shutdown
|
||||
tdata.SetTabTheme += update.SetTabTheme
|
||||
tdata.NumMagnify += update.NumMagnify
|
||||
tdata.NumPanics += update.NumPanics
|
||||
tdata.NumAIReqs += update.NumAIReqs
|
||||
if update.NumTabs > 0 {
|
||||
tdata.NumTabs = update.NumTabs
|
||||
}
|
||||
if update.NumBlocks > 0 {
|
||||
tdata.NumBlocks = update.NumBlocks
|
||||
}
|
||||
if update.NumWindows > 0 {
|
||||
tdata.NumWindows = update.NumWindows
|
||||
}
|
||||
if update.NumWS > 0 {
|
||||
tdata.NumWS = update.NumWS
|
||||
}
|
||||
if update.NumWSNamed > 0 {
|
||||
tdata.NumWSNamed = update.NumWSNamed
|
||||
}
|
||||
if update.NumSSHConn > 0 && update.NumSSHConn > tdata.NumSSHConn {
|
||||
tdata.NumSSHConn = update.NumSSHConn
|
||||
}
|
||||
if update.NumWSLConn > 0 && update.NumWSLConn > tdata.NumWSLConn {
|
||||
tdata.NumWSLConn = update.NumWSLConn
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if len(update.WshCmds) > 0 {
|
||||
if tdata.WshCmds == nil {
|
||||
tdata.WshCmds = make(map[string]int)
|
||||
}
|
||||
for key, val := range update.WshCmds {
|
||||
tdata.WshCmds[key] += val
|
||||
}
|
||||
}
|
||||
if len(update.Conn) > 0 {
|
||||
if tdata.Conn == nil {
|
||||
tdata.Conn = make(map[string]int)
|
||||
}
|
||||
for key, val := range update.Conn {
|
||||
tdata.Conn[key] += val
|
||||
}
|
||||
}
|
||||
if len(update.Displays) > 0 {
|
||||
tdata.Displays = update.Displays
|
||||
}
|
||||
if len(update.Blocks) > 0 {
|
||||
tdata.Blocks = update.Blocks
|
||||
}
|
||||
query = `UPDATE db_activity
|
||||
SET tdata = ?,
|
||||
clientversion = ?,
|
||||
buildtime = ?
|
||||
WHERE day = ?`
|
||||
tx.Exec(query, tdata, wavebase.WaveVersion, wavebase.BuildTime, dayStr)
|
||||
return nil
|
||||
})
|
||||
if txErr != nil {
|
||||
return txErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetNonUploadedActivity(ctx context.Context) ([]*ActivityType, error) {
|
||||
var rtn []*ActivityType
|
||||
txErr := wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {
|
||||
query := `SELECT * FROM db_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
|
||||
}
|
||||
|
||||
func MarkActivityAsUploaded(ctx context.Context, activityArr []*ActivityType) error {
|
||||
dayStr := daystr.GetCurDayStr()
|
||||
txErr := wstore.WithTx(ctx, func(tx *wstore.TxWrap) error {
|
||||
query := `UPDATE db_activity SET uploaded = 1 WHERE day = ?`
|
||||
for _, activity := range activityArr {
|
||||
if activity.Day == dayStr {
|
||||
continue
|
||||
}
|
||||
tx.Exec(query, activity.Day)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return txErr
|
||||
}
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package telemetrydata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
)
|
||||
|
||||
var ValidEventNames = map[string]bool{
|
||||
"app:startup": true,
|
||||
"app:shutdown": true,
|
||||
"app:activity": true,
|
||||
"app:display": true,
|
||||
"app:counts": true,
|
||||
|
||||
"action:magnify": true,
|
||||
"action:settabtheme": true,
|
||||
"action:runaicmd": true,
|
||||
"action:createtab": true,
|
||||
"action:createblock": true,
|
||||
"action:openwaveai": true,
|
||||
"action:other": true,
|
||||
"action:term": true,
|
||||
"action:termdurable": true,
|
||||
"action:link": true,
|
||||
|
||||
"wsh:run": true,
|
||||
|
||||
"debug:panic": true,
|
||||
|
||||
"conn:connect": true,
|
||||
"conn:connecterror": true,
|
||||
"conn:nowsh": true,
|
||||
|
||||
"waveai:enabletelemetry": true,
|
||||
"waveai:post": true,
|
||||
"waveai:feedback": true,
|
||||
"waveai:showdiff": true,
|
||||
"waveai:revertfile": true,
|
||||
|
||||
"onboarding:start": true,
|
||||
"onboarding:skip": true,
|
||||
"onboarding:fire": true,
|
||||
"onboarding:githubstar": true,
|
||||
|
||||
"job:start": true,
|
||||
"job:reconnect": true,
|
||||
"job:done": true,
|
||||
}
|
||||
|
||||
type TEvent struct {
|
||||
Uuid string `json:"uuid,omitempty" db:"uuid"`
|
||||
Ts int64 `json:"ts,omitempty" db:"ts"`
|
||||
TsLocal string `json:"tslocal,omitempty" db:"tslocal"` // iso8601 format (wall clock converted to PT)
|
||||
Event string `json:"event" db:"event"`
|
||||
Props TEventProps `json:"props" db:"-"` // Don't scan directly to map
|
||||
|
||||
// DB fields
|
||||
Uploaded bool `json:"-" db:"uploaded"`
|
||||
|
||||
// For database scanning
|
||||
RawProps string `json:"-" db:"props"`
|
||||
}
|
||||
|
||||
type TEventUserProps struct {
|
||||
ClientArch string `json:"client:arch,omitempty"`
|
||||
ClientVersion string `json:"client:version,omitempty"`
|
||||
ClientInitialVersion string `json:"client:initial_version,omitempty"`
|
||||
ClientBuildTime string `json:"client:buildtime,omitempty"`
|
||||
ClientOSRelease string `json:"client:osrelease,omitempty"`
|
||||
ClientIsDev bool `json:"client:isdev,omitempty"`
|
||||
ClientPackageType string `json:"client:packagetype,omitempty"`
|
||||
ClientMacOSVersion string `json:"client:macos,omitempty"`
|
||||
|
||||
CohortMonth string `json:"cohort:month,omitempty"`
|
||||
CohortISOWeek string `json:"cohort:isoweek,omitempty"`
|
||||
|
||||
AutoUpdateChannel string `json:"autoupdate:channel,omitempty"`
|
||||
AutoUpdateEnabled bool `json:"autoupdate:enabled,omitempty"`
|
||||
|
||||
LocalShellType string `json:"localshell:type,omitempty"`
|
||||
LocalShellVersion string `json:"localshell:version,omitempty"`
|
||||
|
||||
LocCountryCode string `json:"loc:countrycode,omitempty"`
|
||||
LocRegionCode string `json:"loc:regioncode,omitempty"`
|
||||
|
||||
SettingsCustomWidgets int `json:"settings:customwidgets,omitempty"`
|
||||
SettingsCustomAIPresets int `json:"settings:customaipresets,omitempty"`
|
||||
SettingsCustomSettings int `json:"settings:customsettings,omitempty"`
|
||||
SettingsCustomAIModes int `json:"settings:customaimodes,omitempty"`
|
||||
SettingsSecretsCount int `json:"settings:secretscount,omitempty"`
|
||||
SettingsTransparent bool `json:"settings:transparent,omitempty"`
|
||||
}
|
||||
|
||||
type TEventProps struct {
|
||||
TEventUserProps `tstype:"-"` // generally don't need to set these since they will be automatically copied over
|
||||
|
||||
ActiveMinutes int `json:"activity:activeminutes,omitempty"`
|
||||
FgMinutes int `json:"activity:fgminutes,omitempty"`
|
||||
OpenMinutes int `json:"activity:openminutes,omitempty"`
|
||||
WaveAIActiveMinutes int `json:"activity:waveaiactiveminutes,omitempty"`
|
||||
WaveAIFgMinutes int `json:"activity:waveaifgminutes,omitempty"`
|
||||
TermCommandsRun int `json:"activity:termcommandsrun,omitempty"`
|
||||
TermCommandsRemote int `json:"activity:termcommands:remote,omitempty"`
|
||||
TermCommandsDurable int `json:"activity:termcommands:durable,omitempty"`
|
||||
TermCommandsWsl int `json:"activity:termcommands:wsl,omitempty"`
|
||||
|
||||
AppFirstDay bool `json:"app:firstday,omitempty"`
|
||||
AppFirstLaunch bool `json:"app:firstlaunch,omitempty"`
|
||||
|
||||
ActionInitiator string `json:"action:initiator,omitempty" tstype:"\"keyboard\" | \"mouse\""`
|
||||
ActionType string `json:"action:type,omitempty"`
|
||||
|
||||
PanicType string `json:"debug:panictype,omitempty"`
|
||||
|
||||
BlockView string `json:"block:view,omitempty"`
|
||||
BlockController string `json:"block:controller,omitempty"`
|
||||
BlockSubBlock bool `json:"block:subblock,omitempty"`
|
||||
|
||||
AiBackendType string `json:"ai:backendtype,omitempty"`
|
||||
AiLocal bool `json:"ai:local,omitempty"`
|
||||
|
||||
WshCmd string `json:"wsh:cmd,omitempty"`
|
||||
WshErrorCount int `json:"wsh:errorcount,omitempty"`
|
||||
WshCount int `json:"wsh:count,omitempty"`
|
||||
|
||||
ConnType string `json:"conn:conntype,omitempty"`
|
||||
ConnWshErrorCode string `json:"conn:wsherrorcode,omitempty"`
|
||||
ConnErrorCode string `json:"conn:errorcode,omitempty"`
|
||||
ConnSubErrorCode string `json:"conn:suberrorcode,omitempty"`
|
||||
ConnContextError bool `json:"conn:contexterror,omitempty"`
|
||||
|
||||
OnboardingFeature string `json:"onboarding:feature,omitempty" tstype:"\"waveai\" | \"durable\" | \"magnify\" | \"wsh\""`
|
||||
OnboardingVersion string `json:"onboarding:version,omitempty"`
|
||||
OnboardingGithubStar string `json:"onboarding:githubstar,omitempty" tstype:"\"already\" | \"star\" | \"later\""`
|
||||
OnboardingPage string `json:"onboarding:page,omitempty"`
|
||||
|
||||
DisplayHeight int `json:"display:height,omitempty"`
|
||||
DisplayWidth int `json:"display:width,omitempty"`
|
||||
DisplayDPR float64 `json:"display:dpr,omitempty"`
|
||||
DisplayCount int `json:"display:count,omitempty"`
|
||||
DisplayAll interface{} `json:"display:all,omitempty"`
|
||||
|
||||
CountBlocks int `json:"count:blocks,omitempty"`
|
||||
CountTabs int `json:"count:tabs,omitempty"`
|
||||
CountWindows int `json:"count:windows,omitempty"`
|
||||
CountWorkspaces int `json:"count:workspaces,omitempty"`
|
||||
CountSSHConn int `json:"count:sshconn,omitempty"`
|
||||
CountWSLConn int `json:"count:wslconn,omitempty"`
|
||||
CountJobs int `json:"count:jobs,omitempty"`
|
||||
CountJobsConnected int `json:"count:jobsconnected,omitempty"`
|
||||
CountViews map[string]int `json:"count:views,omitempty"`
|
||||
|
||||
WaveAIAPIType string `json:"waveai:apitype,omitempty"`
|
||||
WaveAIModel string `json:"waveai:model,omitempty"`
|
||||
WaveAIChatId string `json:"waveai:chatid,omitempty"`
|
||||
WaveAIStepNum int `json:"waveai:stepnum,omitempty"`
|
||||
WaveAIInputTokens int `json:"waveai:inputtokens,omitempty"`
|
||||
WaveAIOutputTokens int `json:"waveai:outputtokens,omitempty"`
|
||||
WaveAINativeWebSearchCount int `json:"waveai:nativewebsearchcount,omitempty"`
|
||||
WaveAIRequestCount int `json:"waveai:requestcount,omitempty"`
|
||||
WaveAIToolUseCount int `json:"waveai:toolusecount,omitempty"`
|
||||
WaveAIToolUseErrorCount int `json:"waveai:tooluseerrorcount,omitempty"`
|
||||
WaveAIToolDetail map[string]int `json:"waveai:tooldetail,omitempty"`
|
||||
WaveAIProxyReq int `json:"waveai:proxyreq,omitempty"`
|
||||
WaveAIHadError bool `json:"waveai:haderror,omitempty"`
|
||||
WaveAIImageCount int `json:"waveai:imagecount,omitempty"`
|
||||
WaveAIPDFCount int `json:"waveai:pdfcount,omitempty"`
|
||||
WaveAITextDocCount int `json:"waveai:textdoccount,omitempty"`
|
||||
WaveAITextLen int `json:"waveai:textlen,omitempty"`
|
||||
WaveAIFirstByteMs int `json:"waveai:firstbytems,omitempty"` // ms
|
||||
WaveAIRequestDurMs int `json:"waveai:requestdurms,omitempty"` // ms
|
||||
WaveAIWidgetAccess bool `json:"waveai:widgetaccess,omitempty"`
|
||||
WaveAIThinkingLevel string `json:"waveai:thinkinglevel,omitempty"`
|
||||
WaveAIMode string `json:"waveai:mode,omitempty"`
|
||||
WaveAIProvider string `json:"waveai:provider,omitempty"`
|
||||
WaveAIIsLocal bool `json:"waveai:islocal,omitempty"`
|
||||
WaveAIFeedback string `json:"waveai:feedback,omitempty" tstype:"\"good\" | \"bad\""`
|
||||
WaveAIAction string `json:"waveai:action,omitempty"`
|
||||
|
||||
JobDoneReason string `json:"job:donereason,omitempty"`
|
||||
JobKind string `json:"job:kind,omitempty"`
|
||||
|
||||
UserSet *TEventUserProps `json:"$set,omitempty"`
|
||||
UserSetOnce *TEventUserProps `json:"$set_once,omitempty"`
|
||||
}
|
||||
|
||||
func MakeTEvent(event string, props TEventProps) *TEvent {
|
||||
now := time.Now()
|
||||
// TsLocal gets set in EnsureTimestamps()
|
||||
return &TEvent{
|
||||
Uuid: uuid.New().String(),
|
||||
Ts: now.UnixMilli(),
|
||||
Event: event,
|
||||
Props: props,
|
||||
}
|
||||
}
|
||||
|
||||
func MakeUntypedTEvent(event string, propsMap map[string]any) (*TEvent, error) {
|
||||
if event == "" {
|
||||
return nil, fmt.Errorf("event name must be non-empty")
|
||||
}
|
||||
var props TEventProps
|
||||
err := utilfn.ReUnmarshal(&props, propsMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error re-marshalling TEvent props: %w", err)
|
||||
}
|
||||
return MakeTEvent(event, props), nil
|
||||
}
|
||||
|
||||
func (t *TEvent) EnsureTimestamps() {
|
||||
if t.Ts == 0 {
|
||||
t.Ts = time.Now().UnixMilli()
|
||||
}
|
||||
gtime := time.UnixMilli(t.Ts)
|
||||
t.TsLocal = utilfn.ConvertToWallClockPT(gtime).Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (t *TEvent) UserSetProps() *TEventUserProps {
|
||||
if t.Props.UserSet == nil {
|
||||
t.Props.UserSet = &TEventUserProps{}
|
||||
}
|
||||
return t.Props.UserSet
|
||||
}
|
||||
|
||||
func (t *TEvent) UserSetOnceProps() *TEventUserProps {
|
||||
if t.Props.UserSetOnce == nil {
|
||||
t.Props.UserSetOnce = &TEventUserProps{}
|
||||
}
|
||||
return t.Props.UserSetOnce
|
||||
}
|
||||
|
||||
func (t *TEvent) ConvertRawJSON() error {
|
||||
if t.RawProps != "" {
|
||||
return json.Unmarshal([]byte(t.RawProps), &t.Props)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var eventNameRe = regexp.MustCompile(`^[a-zA-Z0-9.:_/-]+$`)
|
||||
|
||||
// validates a tevent that was just created (not for validating out of the DB, or an uploaded TEvent)
|
||||
// checks that TS is pretty current (or unset)
|
||||
func (te *TEvent) Validate(current bool) error {
|
||||
if te == nil {
|
||||
return fmt.Errorf("TEvent cannot be nil")
|
||||
}
|
||||
if te.Event == "" {
|
||||
return fmt.Errorf("TEvent.Event cannot be empty")
|
||||
}
|
||||
if !eventNameRe.MatchString(te.Event) {
|
||||
return fmt.Errorf("TEvent.Event invalid: %q", te.Event)
|
||||
}
|
||||
if !ValidEventNames[te.Event] {
|
||||
return fmt.Errorf("TEvent.Event not valid: %q", te.Event)
|
||||
}
|
||||
if te.Uuid == "" {
|
||||
return fmt.Errorf("TEvent.Uuid cannot be empty")
|
||||
}
|
||||
_, err := uuid.Parse(te.Uuid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TEvent.Uuid invalid: %v", err)
|
||||
}
|
||||
if current {
|
||||
if te.Ts != 0 {
|
||||
now := time.Now().UnixMilli()
|
||||
if te.Ts > now+60000 || te.Ts < now-60000 {
|
||||
return fmt.Errorf("TEvent.Ts is not current: %d", te.Ts)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if te.Ts == 0 {
|
||||
return fmt.Errorf("TEvent.Ts must be set")
|
||||
}
|
||||
if te.TsLocal == "" {
|
||||
return fmt.Errorf("TEvent.TsLocal must be set")
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339, te.TsLocal)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TEvent.TsLocal parse error: %v", err)
|
||||
}
|
||||
now := time.Now()
|
||||
if t.Before(now.Add(-30*24*time.Hour)) || t.After(now.Add(2*24*time.Hour)) {
|
||||
return fmt.Errorf("tslocal out of valid range")
|
||||
}
|
||||
}
|
||||
barr, err := json.Marshal(te.Props)
|
||||
if err != nil {
|
||||
return fmt.Errorf("TEvent.Props JSON error: %v", err)
|
||||
}
|
||||
if len(barr) > 20000 {
|
||||
return fmt.Errorf("TEvent.Props too large: %d", len(barr))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package wcloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/daystr"
|
||||
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
||||
)
|
||||
|
||||
const WCloudEndpoint = "https://api.waveterm.dev/central"
|
||||
const WCloudEndpointVarName = "WCLOUD_ENDPOINT"
|
||||
const WCloudPingEndpoint = "https://ping.waveterm.dev/central"
|
||||
const WCloudPingEndpointVarName = "WCLOUD_PING_ENDPOINT"
|
||||
|
||||
var WCloudEndpoint_VarCache string
|
||||
var WCloudPingEndpoint_VarCache string
|
||||
|
||||
const APIVersion = 1
|
||||
const MaxPtyUpdateSize = (128 * 1024)
|
||||
const MaxUpdatesPerReq = 10
|
||||
const MaxUpdatesToDeDup = 1000
|
||||
const MaxUpdateWriterErrors = 3
|
||||
const WCloudDefaultTimeout = 5 * time.Second
|
||||
const WCloudWebShareUpdateTimeout = 15 * time.Second
|
||||
|
||||
// setting to 1M to be safe (max is 6M for API-GW + Lambda, but there is base64 encoding and upload time)
|
||||
// we allow one extra update past this estimated size
|
||||
const MaxUpdatePayloadSize = 1 * (1024 * 1024)
|
||||
|
||||
const TelemetryUrl = "/telemetry"
|
||||
const TEventsUrl = "/tevents"
|
||||
const NoTelemetryUrl = "/no-telemetry"
|
||||
const WebShareUpdateUrl = "/auth/web-share-update"
|
||||
const PingUrl = "/ping"
|
||||
|
||||
func CacheAndRemoveEnvVars() error {
|
||||
WCloudEndpoint_VarCache = os.Getenv(WCloudEndpointVarName)
|
||||
err := checkEndpointVar(WCloudEndpoint_VarCache, "wcloud endpoint", WCloudEndpointVarName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Unsetenv(WCloudEndpointVarName)
|
||||
WCloudPingEndpoint_VarCache = os.Getenv(WCloudPingEndpointVarName)
|
||||
os.Unsetenv(WCloudPingEndpointVarName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEndpointVar(endpoint string, debugName string, varName string) error {
|
||||
if !wavebase.IsDevMode() {
|
||||
return nil
|
||||
}
|
||||
if endpoint == "" || !strings.HasPrefix(endpoint, "https://") {
|
||||
return fmt.Errorf("invalid %s, %s not set or invalid", debugName, varName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetEndpoint() string {
|
||||
if !wavebase.IsDevMode() {
|
||||
return WCloudEndpoint
|
||||
}
|
||||
endpoint := WCloudEndpoint_VarCache
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func GetPingEndpoint() string {
|
||||
if !wavebase.IsDevMode() {
|
||||
return WCloudPingEndpoint
|
||||
}
|
||||
endpoint := WCloudPingEndpoint_VarCache
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func makeAnonPostReq(ctx context.Context, apiUrl string, data interface{}) (*http.Request, error) {
|
||||
endpoint := GetEndpoint()
|
||||
if endpoint == "" {
|
||||
return nil, errors.New("wcloud endpoint not set")
|
||||
}
|
||||
var dataReader io.Reader
|
||||
if data != nil {
|
||||
byteArr, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling json for %s request: %v", apiUrl, err)
|
||||
}
|
||||
dataReader = bytes.NewReader(byteArr)
|
||||
}
|
||||
fullUrl := GetEndpoint() + apiUrl
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", fullUrl, dataReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating %s request: %v", apiUrl, err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-PromptAPIVersion", strconv.Itoa(APIVersion))
|
||||
req.Header.Set("X-PromptAPIUrl", apiUrl)
|
||||
req.Close = true
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func doRequest(req *http.Request, outputObj interface{}, verbose bool) (*http.Response, error) {
|
||||
apiUrl := req.Header.Get("X-PromptAPIUrl")
|
||||
if verbose {
|
||||
log.Printf("[wcloud] sending request %s %v\n", req.Method, req.URL)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error contacting wcloud %q service: %v", apiUrl, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("error reading %q response body: %v", apiUrl, err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return resp, fmt.Errorf("error contacting wcloud %q service: %s", apiUrl, resp.Status)
|
||||
}
|
||||
if outputObj != nil && resp.Header.Get("Content-Type") == "application/json" {
|
||||
err = json.Unmarshal(bodyBytes, outputObj)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("error decoding json: %v", err)
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
type TEventsInputType struct {
|
||||
ClientId string `json:"clientid"`
|
||||
Events []*telemetrydata.TEvent `json:"events"`
|
||||
}
|
||||
|
||||
const TEventsBatchSize = 200
|
||||
const TEventsMaxBatches = 10
|
||||
|
||||
// returns (done, num-sent, error)
|
||||
func sendTEventsBatch(clientId string) (bool, int, error) {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), WCloudDefaultTimeout)
|
||||
defer cancelFn()
|
||||
events, err := telemetry.GetNonUploadedTEvents(ctx, TEventsBatchSize)
|
||||
if err != nil {
|
||||
return true, 0, fmt.Errorf("cannot get events: %v", err)
|
||||
}
|
||||
if len(events) == 0 {
|
||||
return true, 0, nil
|
||||
}
|
||||
input := TEventsInputType{
|
||||
ClientId: clientId,
|
||||
Events: events,
|
||||
}
|
||||
req, err := makeAnonPostReq(ctx, TEventsUrl, input)
|
||||
if err != nil {
|
||||
return true, 0, err
|
||||
}
|
||||
startTime := time.Now()
|
||||
_, err = doRequest(req, nil, true)
|
||||
latency := time.Since(startTime)
|
||||
log.Printf("[wcloud] sent %d tevents (latency: %v)\n", len(events), latency)
|
||||
if err != nil {
|
||||
return true, 0, err
|
||||
}
|
||||
err = telemetry.MarkTEventsAsUploaded(ctx, events)
|
||||
if err != nil {
|
||||
return true, 0, fmt.Errorf("error marking activity as uploaded: %v", err)
|
||||
}
|
||||
return len(events) < TEventsBatchSize, len(events), nil
|
||||
}
|
||||
|
||||
func sendTEvents(clientId string) (int, error) {
|
||||
numIters := 0
|
||||
totalEvents := 0
|
||||
for {
|
||||
numIters++
|
||||
done, numEvents, err := sendTEventsBatch(clientId)
|
||||
if err != nil {
|
||||
log.Printf("error sending telemetry events: %v\n", err)
|
||||
break
|
||||
}
|
||||
totalEvents += numEvents
|
||||
if done {
|
||||
break
|
||||
}
|
||||
if numIters > TEventsMaxBatches {
|
||||
log.Printf("sendTEvents, hit %d iterations, stopping\n", numIters)
|
||||
break
|
||||
}
|
||||
}
|
||||
return totalEvents, nil
|
||||
}
|
||||
|
||||
func SendAllTelemetry(clientId string) error {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancelFn()
|
||||
if err := telemetry.CleanOldTEvents(ctx); err != nil {
|
||||
log.Printf("error cleaning old telemetry events: %v\n", err)
|
||||
}
|
||||
if !telemetry.IsTelemetryEnabled() {
|
||||
log.Printf("telemetry disabled, not sending\n")
|
||||
return nil
|
||||
}
|
||||
_, err := sendTEvents(clientId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendTelemetry(clientId string) error {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), WCloudDefaultTimeout)
|
||||
defer cancelFn()
|
||||
activity, err := telemetry.GetNonUploadedActivity(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get activity: %v", err)
|
||||
}
|
||||
if len(activity) == 0 {
|
||||
return nil
|
||||
}
|
||||
log.Printf("[wcloud] sending telemetry data\n")
|
||||
dayStr := daystr.GetCurDayStr()
|
||||
input := TelemetryInputType{
|
||||
ClientId: clientId,
|
||||
UserId: clientId,
|
||||
AppType: "w2",
|
||||
AutoUpdateEnabled: telemetry.IsAutoUpdateEnabled(),
|
||||
AutoUpdateChannel: telemetry.AutoUpdateChannel(),
|
||||
CurDay: dayStr,
|
||||
Activity: activity,
|
||||
}
|
||||
req, err := makeAnonPostReq(ctx, TelemetryUrl, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = doRequest(req, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = telemetry.MarkActivityAsUploaded(ctx, activity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marking activity as uploaded: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SendNoTelemetryUpdate(ctx context.Context, clientId string, noTelemetryVal bool) error {
|
||||
req, err := makeAnonPostReq(ctx, NoTelemetryUrl, NoTelemetryInputType{ClientId: clientId, Value: noTelemetryVal})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = doRequest(req, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func makePingPostReq(ctx context.Context, apiUrl string, data interface{}) (*http.Request, error) {
|
||||
endpoint := GetPingEndpoint()
|
||||
if endpoint == "" {
|
||||
return nil, errors.New("wcloud ping endpoint not set")
|
||||
}
|
||||
var dataReader io.Reader
|
||||
if data != nil {
|
||||
byteArr, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error marshaling json for %s request: %v", apiUrl, err)
|
||||
}
|
||||
dataReader = bytes.NewReader(byteArr)
|
||||
}
|
||||
fullUrl := endpoint + apiUrl
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", fullUrl, dataReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating %s request: %v", apiUrl, err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-PromptAPIVersion", strconv.Itoa(APIVersion))
|
||||
req.Close = true
|
||||
return req, nil
|
||||
}
|
||||
|
||||
type PingInputType struct {
|
||||
ClientId string `json:"clientid"`
|
||||
Arch string `json:"arch"`
|
||||
Version string `json:"version"`
|
||||
LocalDate string `json:"localdate"`
|
||||
UsageTelemetry bool `json:"usagetelemetry"`
|
||||
}
|
||||
|
||||
func SendDiagnosticPing(ctx context.Context, clientId string, usageTelemetry bool) error {
|
||||
endpoint := GetPingEndpoint()
|
||||
if endpoint == "" {
|
||||
return nil
|
||||
}
|
||||
localDate := time.Now().Format("2006-01-02")
|
||||
input := PingInputType{
|
||||
ClientId: clientId,
|
||||
Arch: wavebase.ClientArch(),
|
||||
Version: "v" + wavebase.WaveVersion,
|
||||
LocalDate: localDate,
|
||||
UsageTelemetry: usageTelemetry,
|
||||
}
|
||||
req, err := makePingPostReq(ctx, PingUrl, input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = doRequest(req, nil, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright 2025, Command Line Inc.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package wcloud
|
||||
|
||||
import (
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
)
|
||||
|
||||
type NoTelemetryInputType struct {
|
||||
ClientId string `json:"clientid"`
|
||||
Value bool `json:"value"`
|
||||
}
|
||||
|
||||
type TelemetryInputType struct {
|
||||
UserId string `json:"userid"`
|
||||
ClientId string `json:"clientid"`
|
||||
AppType string `json:"apptype,omitempty"`
|
||||
AutoUpdateEnabled bool `json:"autoupdateenabled,omitempty"`
|
||||
AutoUpdateChannel string `json:"autoupdatechannel,omitempty"`
|
||||
CurDay string `json:"curday"`
|
||||
Activity []*telemetry.ActivityType `json:"activity"`
|
||||
}
|
||||
|
|
@ -29,7 +29,6 @@
|
|||
"window:magnifiedblockblursecondarypx": 2,
|
||||
"window:confirmclose": true,
|
||||
"window:savelastwindow": true,
|
||||
"telemetry:enabled": true,
|
||||
"term:bellsound": false,
|
||||
"term:bellindicator": true,
|
||||
"term:osc52": "always",
|
||||
|
|
|
|||
|
|
@ -112,9 +112,6 @@ const (
|
|||
ConfigKey_WindowDimensions = "window:dimensions"
|
||||
ConfigKey_WindowZoom = "window:zoom"
|
||||
|
||||
ConfigKey_TelemetryClear = "telemetry:*"
|
||||
ConfigKey_TelemetryEnabled = "telemetry:enabled"
|
||||
|
||||
ConfigKey_ConnClear = "conn:*"
|
||||
ConfigKey_ConnAskBeforeWshInstall = "conn:askbeforewshinstall"
|
||||
ConfigKey_ConnWshEnabled = "conn:wshenabled"
|
||||
|
|
|
|||
|
|
@ -163,9 +163,6 @@ type SettingsType struct {
|
|||
WindowDimensions string `json:"window:dimensions,omitempty"`
|
||||
WindowZoom *float64 `json:"window:zoom,omitempty"`
|
||||
|
||||
TelemetryClear bool `json:"telemetry:*,omitempty"`
|
||||
TelemetryEnabled bool `json:"telemetry:enabled,omitempty"`
|
||||
|
||||
ConnClear bool `json:"conn:*,omitempty"`
|
||||
ConnAskBeforeWshInstall *bool `json:"conn:askbeforewshinstall,omitempty"`
|
||||
ConnWshEnabled bool `json:"conn:wshenabled,omitempty"`
|
||||
|
|
@ -1001,7 +998,7 @@ func (fc *FullConfigType) CountCustomAIModes() int {
|
|||
}
|
||||
|
||||
// CountCustomSettings returns the number of settings in the user's settings file.
|
||||
// This excludes telemetry:enabled and autoupdate:channel which don't count as customizations.
|
||||
// This excludes autoupdate:channel which doesn't count as a customization.
|
||||
func CountCustomSettings() int {
|
||||
// Load user settings
|
||||
userSettings, _ := ReadWaveHomeConfigFile("settings.json")
|
||||
|
|
@ -1009,10 +1006,10 @@ func CountCustomSettings() int {
|
|||
return 0
|
||||
}
|
||||
|
||||
// Count all keys except telemetry:enabled and autoupdate:channel
|
||||
// Count all keys except autoupdate:channel
|
||||
count := 0
|
||||
for key := range userSettings {
|
||||
if key == "telemetry:enabled" || key == "autoupdate:channel" {
|
||||
if key == "autoupdate:channel" {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
|
|
|
|||
|
|
@ -7,13 +7,9 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/pkg/filestore"
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||
|
|
@ -32,9 +28,6 @@ func CreateSubBlock(ctx context.Context, blockId string, blockDef *waveobj.Block
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating sub block: %w", err)
|
||||
}
|
||||
blockView := blockDef.Meta.GetString(waveobj.MetaKey_View, "")
|
||||
blockController := blockDef.Meta.GetString(waveobj.MetaKey_Controller, "")
|
||||
go recordBlockCreationTelemetry(blockView, blockController, true)
|
||||
return blockData, nil
|
||||
}
|
||||
|
||||
|
|
@ -59,10 +52,6 @@ func createSubBlockObj(ctx context.Context, parentBlockId string, blockDef *wave
|
|||
}
|
||||
|
||||
func CreateBlock(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (rtnBlock *waveobj.Block, rtnErr error) {
|
||||
return CreateBlockWithTelemetry(ctx, tabId, blockDef, rtOpts, true)
|
||||
}
|
||||
|
||||
func CreateBlockWithTelemetry(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts, recordTelemetry bool) (rtnBlock *waveobj.Block, rtnErr error) {
|
||||
var blockCreated bool
|
||||
var newBlockOID string
|
||||
defer func() {
|
||||
|
|
@ -100,36 +89,9 @@ func CreateBlockWithTelemetry(ctx context.Context, tabId string, blockDef *waveo
|
|||
}
|
||||
}
|
||||
}
|
||||
if recordTelemetry {
|
||||
blockView := blockDef.Meta.GetString(waveobj.MetaKey_View, "")
|
||||
blockController := blockDef.Meta.GetString(waveobj.MetaKey_Controller, "")
|
||||
go recordBlockCreationTelemetry(blockView, blockController, false)
|
||||
}
|
||||
return blockData, nil
|
||||
}
|
||||
|
||||
func recordBlockCreationTelemetry(blockView string, blockController string, subBlock bool) {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("CreateBlock:telemetry", recover())
|
||||
}()
|
||||
if blockView == "" {
|
||||
return
|
||||
}
|
||||
tctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancelFn()
|
||||
telemetry.UpdateActivity(tctx, wshrpc.ActivityUpdate{
|
||||
Renderers: map[string]int{blockView: 1},
|
||||
})
|
||||
telemetry.RecordTEvent(tctx, &telemetrydata.TEvent{
|
||||
Event: "action:createblock",
|
||||
Props: telemetrydata.TEventProps{
|
||||
BlockView: blockView,
|
||||
BlockController: blockController,
|
||||
BlockSubBlock: subBlock,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func createBlockObj(ctx context.Context, tabId string, blockDef *waveobj.BlockDef, rtOpts *waveobj.RuntimeOpts) (*waveobj.Block, error) {
|
||||
return wstore.WithTxRtn(ctx, func(tx *wstore.TxWrap) (*waveobj.Block, error) {
|
||||
tab, _ := wstore.DBGet[*waveobj.Tab](tx.Context(), tabId)
|
||||
|
|
|
|||
|
|
@ -102,13 +102,13 @@ func QueueLayoutActionForTab(ctx context.Context, tabId string, actions ...waveo
|
|||
return QueueLayoutAction(ctx, layoutStateId, actions...)
|
||||
}
|
||||
|
||||
func ApplyPortableLayout(ctx context.Context, tabId string, layout PortableLayout, recordTelemetry bool) error {
|
||||
func ApplyPortableLayout(ctx context.Context, tabId string, layout PortableLayout) error {
|
||||
actions := make([]waveobj.LayoutActionData, len(layout)+1)
|
||||
actions[0] = waveobj.LayoutActionData{ActionType: LayoutActionDataType_ClearTree}
|
||||
for i := 0; i < len(layout); i++ {
|
||||
layoutAction := layout[i]
|
||||
|
||||
blockData, err := CreateBlockWithTelemetry(ctx, tabId, layoutAction.BlockDef, &waveobj.RuntimeOpts{}, recordTelemetry)
|
||||
blockData, err := CreateBlock(ctx, tabId, layoutAction.BlockDef, &waveobj.RuntimeOpts{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create block to apply portable layout to tab %s: %w", tabId, err)
|
||||
}
|
||||
|
|
@ -158,7 +158,7 @@ func BootstrapStarterLayout(ctx context.Context) error {
|
|||
tabId := workspace.ActiveTabId
|
||||
|
||||
starterLayout := GetStarterLayout()
|
||||
err = ApplyPortableLayout(ctx, tabId, starterLayout, false)
|
||||
err = ApplyPortableLayout(ctx, tabId, starterLayout)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error applying starter layout: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,10 +17,8 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/wavejwt"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wcloud"
|
||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||
"github.com/wavetermdev/waveterm/pkg/wstore"
|
||||
)
|
||||
|
|
@ -145,22 +143,6 @@ func ResolveBlockIdFromPrefix(ctx context.Context, tabId string, blockIdPrefix s
|
|||
return "", fmt.Errorf("widget_id not found: %q", blockIdPrefix)
|
||||
}
|
||||
|
||||
func GoSendNoTelemetryUpdate(telemetryEnabled bool) {
|
||||
go func() {
|
||||
defer func() {
|
||||
panichandler.PanicHandler("GoSendNoTelemetryUpdate", recover())
|
||||
}()
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelFn()
|
||||
clientId := wstore.GetClientId()
|
||||
err := wcloud.SendNoTelemetryUpdate(ctx, clientId, !telemetryEnabled)
|
||||
if err != nil {
|
||||
log.Printf("[error] sending no-telemetry update: %v\n", err)
|
||||
return
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func InitMainServer() error {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelFn()
|
||||
|
|
|
|||
|
|
@ -13,13 +13,10 @@ import (
|
|||
|
||||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/pkg/eventbus"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||
"github.com/wavetermdev/waveterm/pkg/wshrpc"
|
||||
"github.com/wavetermdev/waveterm/pkg/wstore"
|
||||
)
|
||||
|
||||
|
|
@ -250,7 +247,7 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate
|
|||
|
||||
// No need to apply an initial layout for the initial launch, since the starter layout will get applied after onboarding modal dismissal
|
||||
if !isInitialLaunch {
|
||||
err = ApplyPortableLayout(ctx, tab.OID, GetNewTabLayout(), true)
|
||||
err = ApplyPortableLayout(ctx, tab.OID, GetNewTabLayout())
|
||||
if err != nil {
|
||||
return tab.OID, fmt.Errorf("error applying new tab layout: %w", err)
|
||||
}
|
||||
|
|
@ -260,10 +257,6 @@ func CreateTab(ctx context.Context, workspaceId string, tabName string, activate
|
|||
wstore.UpdateObjectMeta(ctx, *tabORef, waveobj.MetaMapType{waveobj.MetaKey_TabBackground: tabBg}, false)
|
||||
}
|
||||
}
|
||||
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{NewTab: 1}, "createtab")
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "action:createtab",
|
||||
})
|
||||
return tab.OID, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ package wshclient
|
|||
import (
|
||||
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
|
||||
"github.com/wavetermdev/waveterm/pkg/baseds"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||
|
|
@ -17,12 +16,6 @@ import (
|
|||
"github.com/wavetermdev/waveterm/pkg/wshutil"
|
||||
)
|
||||
|
||||
// command "activity", wshserver.ActivityCommand
|
||||
func ActivityCommand(w *wshutil.WshRpc, data wshrpc.ActivityUpdate, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "activity", data, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// command "aisendmessage", wshserver.AiSendMessageCommand
|
||||
func AiSendMessageCommand(w *wshutil.WshRpc, data wshrpc.AiMessageData, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "aisendmessage", data, opts)
|
||||
|
|
@ -610,12 +603,6 @@ func PathCommand(w *wshutil.WshRpc, data wshrpc.PathCommandData, opts *wshrpc.Rp
|
|||
return resp, err
|
||||
}
|
||||
|
||||
// command "recordtevent", wshserver.RecordTEventCommand
|
||||
func RecordTEventCommand(w *wshutil.WshRpc, data telemetrydata.TEvent, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "recordtevent", data, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// command "remotedisconnectfromjobmanager", wshserver.RemoteDisconnectFromJobManagerCommand
|
||||
func RemoteDisconnectFromJobManagerCommand(w *wshutil.WshRpc, data wshrpc.CommandRemoteDisconnectFromJobManagerData, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "remotedisconnectfromjobmanager", data, opts)
|
||||
|
|
@ -752,12 +739,6 @@ func RouteUnannounceCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// command "sendtelemetry", wshserver.SendTelemetryCommand
|
||||
func SendTelemetryCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "sendtelemetry", nil, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// command "setblockfocus", wshserver.SetBlockFocusCommand
|
||||
func SetBlockFocusCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "setblockfocus", data, opts)
|
||||
|
|
@ -898,12 +879,6 @@ func WaveAIAddContextCommand(w *wshutil.WshRpc, data wshrpc.CommandWaveAIAddCont
|
|||
return err
|
||||
}
|
||||
|
||||
// command "waveaienabletelemetry", wshserver.WaveAIEnableTelemetryCommand
|
||||
func WaveAIEnableTelemetryCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "waveaienabletelemetry", nil, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// command "waveaigettooldiff", wshserver.WaveAIGetToolDiffCommand
|
||||
func WaveAIGetToolDiffCommand(w *wshutil.WshRpc, data wshrpc.CommandWaveAIGetToolDiffData, opts *wshrpc.RpcOpts) (*wshrpc.CommandWaveAIGetToolDiffRtnData, error) {
|
||||
resp, err := sendRpcRequestCallHelper[*wshrpc.CommandWaveAIGetToolDiffRtnData](w, "waveaigettooldiff", data, opts)
|
||||
|
|
@ -946,12 +921,6 @@ func WriteTempFileCommand(w *wshutil.WshRpc, data wshrpc.CommandWriteTempFileDat
|
|||
return resp, err
|
||||
}
|
||||
|
||||
// command "wshactivity", wshserver.WshActivityCommand
|
||||
func WshActivityCommand(w *wshutil.WshRpc, data map[string]int, opts *wshrpc.RpcOpts) error {
|
||||
_, err := sendRpcRequestCallHelper[any](w, "wshactivity", data, opts)
|
||||
return err
|
||||
}
|
||||
|
||||
// command "wsldefaultdistro", wshserver.WslDefaultDistroCommand
|
||||
func WslDefaultDistroCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (string, error) {
|
||||
resp, err := sendRpcRequestCallHelper[string](w, "wsldefaultdistro", nil, opts)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
"github.com/google/uuid"
|
||||
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
|
||||
"github.com/wavetermdev/waveterm/pkg/baseds"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/vdom"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||
|
|
@ -84,14 +83,10 @@ type WshRpcInterface interface {
|
|||
BlocksListCommand(ctx context.Context, data BlocksListRequest) ([]BlocksListEntry, error)
|
||||
WaveInfoCommand(ctx context.Context) (*WaveInfoData, error)
|
||||
MacOSVersionCommand(ctx context.Context) (string, error)
|
||||
WshActivityCommand(ct context.Context, data map[string]int) error
|
||||
ActivityCommand(ctx context.Context, data ActivityUpdate) error
|
||||
RecordTEventCommand(ctx context.Context, data telemetrydata.TEvent) error
|
||||
GetVarCommand(ctx context.Context, data CommandVarData) (*CommandVarResponseData, error)
|
||||
GetAllVarsCommand(ctx context.Context, data CommandVarData) ([]CommandVarResponseData, error)
|
||||
SetVarCommand(ctx context.Context, data CommandVarData) error
|
||||
PathCommand(ctx context.Context, data PathCommandData) (string, error)
|
||||
SendTelemetryCommand(ctx context.Context) error
|
||||
FetchSuggestionsCommand(ctx context.Context, data FetchSuggestionsData) (*FetchSuggestionsResponse, error)
|
||||
DisposeSuggestionsCommand(ctx context.Context, widgetId string) error
|
||||
GetTabCommand(ctx context.Context, tabId string) (*waveobj.Tab, error)
|
||||
|
|
@ -155,7 +150,6 @@ type WshRpcInterface interface {
|
|||
|
||||
// ai
|
||||
AiSendMessageCommand(ctx context.Context, data AiMessageData) error
|
||||
WaveAIEnableTelemetryCommand(ctx context.Context) error
|
||||
GetWaveAIChatCommand(ctx context.Context, data CommandGetWaveAIChatData) (*uctypes.UIChat, error)
|
||||
GetWaveAIRateLimitCommand(ctx context.Context) (*uctypes.RateLimitInfo, error)
|
||||
WaveAIToolApproveCommand(ctx context.Context, data CommandWaveAIToolApproveData) error
|
||||
|
|
@ -564,42 +558,6 @@ type PathCommandData struct {
|
|||
TabId string `json:"tabid"`
|
||||
}
|
||||
|
||||
type ActivityDisplayType struct {
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
DPR float64 `json:"dpr"`
|
||||
Internal bool `json:"internal,omitempty"`
|
||||
}
|
||||
|
||||
type ActivityUpdate struct {
|
||||
FgMinutes int `json:"fgminutes,omitempty"`
|
||||
ActiveMinutes int `json:"activeminutes,omitempty"`
|
||||
OpenMinutes int `json:"openminutes,omitempty"`
|
||||
WaveAIFgMinutes int `json:"waveaifgminutes,omitempty"`
|
||||
WaveAIActiveMinutes int `json:"waveaiactiveminutes,omitempty"`
|
||||
NumTabs int `json:"numtabs,omitempty"`
|
||||
NewTab int `json:"newtab,omitempty"`
|
||||
NumBlocks int `json:"numblocks,omitempty"`
|
||||
NumWindows int `json:"numwindows,omitempty"`
|
||||
NumWS int `json:"numws,omitempty"`
|
||||
NumWSNamed int `json:"numwsnamed,omitempty"`
|
||||
NumSSHConn int `json:"numsshconn,omitempty"`
|
||||
NumWSLConn int `json:"numwslconn,omitempty"`
|
||||
NumMagnify int `json:"nummagnify,omitempty"`
|
||||
TermCommandsRun int `json:"termcommandsrun,omitempty"`
|
||||
NumPanics int `json:"numpanics,omitempty"`
|
||||
NumAIReqs int `json:"numaireqs,omitempty"`
|
||||
Startup int `json:"startup,omitempty"`
|
||||
Shutdown int `json:"shutdown,omitempty"`
|
||||
SetTabTheme int `json:"settabtheme,omitempty"`
|
||||
BuildTime string `json:"buildtime,omitempty"`
|
||||
Displays []ActivityDisplayType `json:"displays,omitempty"`
|
||||
Renderers map[string]int `json:"renderers,omitempty"`
|
||||
Blocks map[string]int `json:"blocks,omitempty"`
|
||||
WshCmds map[string]int `json:"wshcmds,omitempty"`
|
||||
Conn map[string]int `json:"conn,omitempty"`
|
||||
}
|
||||
|
||||
type ConnExtData struct {
|
||||
ConnName string `json:"connname"`
|
||||
LogBlockId string `json:"logblockid,omitempty"`
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
|
@ -35,15 +34,12 @@ import (
|
|||
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/wshfs"
|
||||
"github.com/wavetermdev/waveterm/pkg/secretstore"
|
||||
"github.com/wavetermdev/waveterm/pkg/suggestion"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/envutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
"github.com/wavetermdev/waveterm/pkg/wavebase"
|
||||
"github.com/wavetermdev/waveterm/pkg/wavejwt"
|
||||
"github.com/wavetermdev/waveterm/pkg/waveobj"
|
||||
"github.com/wavetermdev/waveterm/pkg/wcloud"
|
||||
"github.com/wavetermdev/waveterm/pkg/wconfig"
|
||||
"github.com/wavetermdev/waveterm/pkg/wcore"
|
||||
"github.com/wavetermdev/waveterm/pkg/wps"
|
||||
|
|
@ -985,44 +981,6 @@ func (ws *WshServer) WaveFileReadStreamCommand(ctx context.Context, data wshrpc.
|
|||
return rtnInfo, nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) RecordTEventCommand(ctx context.Context, data telemetrydata.TEvent) error {
|
||||
err := telemetry.RecordTEvent(ctx, &data)
|
||||
if err != nil {
|
||||
log.Printf("error recording telemetry event: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (ws WshServer) SendTelemetryCommand(ctx context.Context) error {
|
||||
return wcloud.SendAllTelemetry(wstore.GetClientId())
|
||||
}
|
||||
|
||||
func (ws *WshServer) WaveAIEnableTelemetryCommand(ctx context.Context) error {
|
||||
// Enable telemetry in config
|
||||
meta := waveobj.MetaMapType{
|
||||
wconfig.ConfigKey_TelemetryEnabled: true,
|
||||
}
|
||||
err := wconfig.SetBaseConfigValue(meta)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting telemetry enabled: %w", err)
|
||||
}
|
||||
|
||||
// Record the telemetry event
|
||||
event := telemetrydata.MakeTEvent("waveai:enabletelemetry", telemetrydata.TEventProps{})
|
||||
err = telemetry.RecordTEvent(ctx, event)
|
||||
if err != nil {
|
||||
log.Printf("error recording waveai:enabletelemetry event: %v", err)
|
||||
}
|
||||
|
||||
// Immediately send telemetry to cloud
|
||||
err = wcloud.SendAllTelemetry(wstore.GetClientId())
|
||||
if err != nil {
|
||||
log.Printf("error sending telemetry after enabling: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) GetWaveAIChatCommand(ctx context.Context, data wshrpc.CommandGetWaveAIChatData) (*uctypes.UIChat, error) {
|
||||
aiChat := chatstore.DefaultChatStore.Get(data.ChatId)
|
||||
if aiChat == nil {
|
||||
|
|
@ -1055,46 +1013,6 @@ func (ws *WshServer) WaveAIGetToolDiffCommand(ctx context.Context, data wshrpc.C
|
|||
}, nil
|
||||
}
|
||||
|
||||
var wshActivityRe = regexp.MustCompile(`^[a-z:#]+$`)
|
||||
|
||||
func (ws *WshServer) WshActivityCommand(ctx context.Context, data map[string]int) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
props := telemetrydata.TEventProps{}
|
||||
for key, value := range data {
|
||||
if len(key) > 20 {
|
||||
delete(data, key)
|
||||
}
|
||||
if !wshActivityRe.MatchString(key) {
|
||||
delete(data, key)
|
||||
}
|
||||
if value != 1 {
|
||||
delete(data, key)
|
||||
}
|
||||
if strings.HasSuffix(key, "#error") {
|
||||
props.WshCmd = strings.TrimSuffix(key, "#error")
|
||||
props.WshErrorCount = 1
|
||||
} else {
|
||||
props.WshCmd = key
|
||||
}
|
||||
}
|
||||
activityUpdate := wshrpc.ActivityUpdate{
|
||||
WshCmds: data,
|
||||
}
|
||||
telemetry.GoUpdateActivityWrap(activityUpdate, "wsh-activity")
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: telemetry.WshRunEventName,
|
||||
Props: props,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) ActivityCommand(ctx context.Context, activity wshrpc.ActivityUpdate) error {
|
||||
telemetry.GoUpdateActivityWrap(activity, "wshrpc-activity")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WshServer) GetVarCommand(ctx context.Context, data wshrpc.CommandVarData) (*wshrpc.CommandVarResponseData, error) {
|
||||
_, fileData, err := filestore.WFS.ReadFile(ctx, data.ZoneId, data.FileName)
|
||||
if err == fs.ErrNotExist {
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ import (
|
|||
"github.com/wavetermdev/waveterm/pkg/genconn"
|
||||
"github.com/wavetermdev/waveterm/pkg/panichandler"
|
||||
"github.com/wavetermdev/waveterm/pkg/remote/conncontroller"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry"
|
||||
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
|
||||
"github.com/wavetermdev/waveterm/pkg/userinput"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
|
||||
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
||||
|
|
@ -512,15 +510,6 @@ func (conn *WslConn) Connect(ctx context.Context) error {
|
|||
conn.Status = Status_Error
|
||||
conn.Error = err.Error()
|
||||
conn.close_nolock()
|
||||
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
|
||||
Conn: map[string]int{"wsl:connecterror": 1},
|
||||
}, "wsl-connconnect")
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "conn:connecterror",
|
||||
Props: telemetrydata.TEventProps{
|
||||
ConnType: "wsl",
|
||||
},
|
||||
})
|
||||
} else {
|
||||
conn.Infof(ctx, "successfully connected (wsh:%v)\n\n", conn.WshEnabled.Load())
|
||||
conn.Status = Status_Connected
|
||||
|
|
@ -528,15 +517,6 @@ func (conn *WslConn) Connect(ctx context.Context) error {
|
|||
if conn.ActiveConnNum == 0 {
|
||||
conn.ActiveConnNum = int(activeConnCounter.Add(1))
|
||||
}
|
||||
telemetry.GoUpdateActivityWrap(wshrpc.ActivityUpdate{
|
||||
Conn: map[string]int{"wsl:connect": 1},
|
||||
}, "wsl-connconnect")
|
||||
telemetry.GoRecordTEventWrap(&telemetrydata.TEvent{
|
||||
Event: "conn:connect",
|
||||
Props: telemetrydata.TEventProps{
|
||||
ConnType: "wsl",
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
conn.FireConnChangeEvent()
|
||||
|
|
|
|||
|
|
@ -307,12 +307,6 @@
|
|||
"window:zoom": {
|
||||
"type": "number"
|
||||
},
|
||||
"telemetry:*": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"telemetry:enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"conn:*": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue