Remove telemetry

This commit is contained in:
Kirill Trofimov 2026-05-10 20:55:31 +03:00
parent fb06108fcb
commit 988a0d6401
86 changed files with 91 additions and 3115 deletions

View file

@ -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",

View file

@ -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()

View file

@ -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 {

View file

@ -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,
}

View file

@ -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() {

View file

@ -0,0 +1,2 @@
-- This migration cannot be reversed
-- Telemetry tables have been permanently removed

View file

@ -0,0 +1,3 @@
-- Drop telemetry tables
DROP TABLE IF EXISTS db_tevent;
DROP TABLE IF EXISTS db_activity;

View file

@ -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",

View file

@ -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.

View file

@ -78,7 +78,6 @@ Other References:
- [Configuration](./config)
- [Custom Widgets](./customwidgets)
- [Full wsh reference](./wsh-reference)
- [Telemetry](./telemetry)
- [FAQ](./faq)
- [Release Notes](./releasenotes)

View file

@ -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 &mdash; Sep 20, 2024

View file

@ -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).

View file

@ -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 Waves 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).

View file

@ -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`:

View file

@ -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:**

View file

@ -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;
}

View file

@ -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();
});

View file

@ -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);

View file

@ -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,

View file

@ -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", () => {

View file

@ -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();

View file

@ -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"),
});

View file

@ -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";

View file

@ -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 = () => {

View file

@ -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);
});

View file

@ -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" });

View file

@ -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>
);

View file

@ -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"

View file

@ -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>

View file

@ -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();
};

View file

@ -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 };

View file

@ -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();
}

View file

@ -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);

View file

@ -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"];

View file

@ -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]);

View file

@ -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;

View file

@ -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));
};

View file

@ -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}

View file

@ -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

View file

@ -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 (

View file

@ -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();
};

View file

@ -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),

View file

@ -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),

View file

@ -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>

View file

@ -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 };

View file

@ -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 };

View file

@ -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,

View file

@ -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;
}

View file

@ -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();

View file

@ -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);

View file

@ -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"];

View file

@ -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"];

View file

@ -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");
}),
});
}

View file

@ -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"];
};

View file

@ -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;

View file

@ -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")) {

View file

@ -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, "");

View file

@ -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"];

View file

@ -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, {

View file

@ -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;
}

View file

@ -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) => "",
};

View 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} />

View file

@ -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 = {

View file

@ -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;

View file

@ -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"`

View file

@ -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"`

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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"`
}

View file

@ -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",

View file

@ -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"

View file

@ -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++

View file

@ -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)

View file

@ -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)
}

View file

@ -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()

View file

@ -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
}

View file

@ -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)

View file

@ -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"`

View file

@ -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 {

View file

@ -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()

View file

@ -307,12 +307,6 @@
"window:zoom": {
"type": "number"
},
"telemetry:*": {
"type": "boolean"
},
"telemetry:enabled": {
"type": "boolean"
},
"conn:*": {
"type": "boolean"
},