Skip to content

Commit d7a9006

Browse files
authored
new tevents analytics framework (#1894)
1 parent 8febd09 commit d7a9006

File tree

32 files changed

+909
-41
lines changed

32 files changed

+909
-41
lines changed

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Targeting 1/31/25
3838

3939
## v0.12
4040

41-
Targeting mid-February (more will get added before work on v0.12 kicks off)
41+
Targeting mid-February.
4242

4343
- 🔷 Import/Export Tab Layouts and Widgets
4444
- 🔷 log viewer

Taskfile.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ tasks:
2626
- docsite:build:embedded
2727
- build:backend
2828
env:
29-
WCLOUD_ENDPOINT: "https://ot2e112zx5.execute-api.us-west-2.amazonaws.com/dev"
30-
WCLOUD_WS_ENDPOINT: "wss://5lfzlg5crl.execute-api.us-west-2.amazonaws.com/dev/"
29+
WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central"
30+
WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev/"
3131

3232
electron:start:
3333
desc: Run the Electron application directly.
@@ -39,8 +39,8 @@ tasks:
3939
- docsite:build:embedded
4040
- build:backend
4141
env:
42-
WCLOUD_ENDPOINT: "https://ot2e112zx5.execute-api.us-west-2.amazonaws.com/dev"
43-
WCLOUD_WS_ENDPOINT: "wss://5lfzlg5crl.execute-api.us-west-2.amazonaws.com/dev/"
42+
WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev"
43+
WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev"
4444

4545
storybook:
4646
desc: Start the Storybook server.

cmd/generatego/main-generatego.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func GenerateWshClient() error {
2424
fmt.Fprintf(os.Stderr, "generating wshclient file to %s\n", WshClientFileName)
2525
var buf strings.Builder
2626
gogen.GenerateBoilerplate(&buf, "wshclient", []string{
27+
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata",
2728
"github.com/wavetermdev/waveterm/pkg/wshutil",
2829
"github.com/wavetermdev/waveterm/pkg/wshrpc",
2930
"github.com/wavetermdev/waveterm/pkg/wconfig",

cmd/server/main-server.go

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import (
2222
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/wshfs"
2323
"github.com/wavetermdev/waveterm/pkg/service"
2424
"github.com/wavetermdev/waveterm/pkg/telemetry"
25+
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
2526
"github.com/wavetermdev/waveterm/pkg/util/shellutil"
2627
"github.com/wavetermdev/waveterm/pkg/util/sigutil"
28+
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
2729
"github.com/wavetermdev/waveterm/pkg/wavebase"
2830
"github.com/wavetermdev/waveterm/pkg/waveobj"
2931
"github.com/wavetermdev/waveterm/pkg/wcloud"
@@ -46,6 +48,8 @@ var BuildTime = "0"
4648
const InitialTelemetryWait = 10 * time.Second
4749
const TelemetryTick = 2 * time.Minute
4850
const TelemetryInterval = 4 * time.Hour
51+
const TelemetryInitialCountsWait = 5 * time.Second
52+
const TelemetryCountsInterval = 1 * time.Hour
4953

5054
var shutdownOnce sync.Once
5155

@@ -82,7 +86,7 @@ func stdinReadWatch() {
8286
}
8387
}
8488

85-
func configWatcher() {
89+
func startConfigWatcher() {
8690
watcher := wconfig.GetWatcher()
8791
if watcher != nil {
8892
watcher.Start()
@@ -101,32 +105,73 @@ func telemetryLoop() {
101105
}
102106
}
103107

104-
func panicTelemetryHandler() {
108+
func panicTelemetryHandler(panicName string) {
105109
activity := wshrpc.ActivityUpdate{NumPanics: 1}
106110
err := telemetry.UpdateActivity(context.Background(), activity)
107111
if err != nil {
108112
log.Printf("error updating activity (panicTelemetryHandler): %v\n", err)
109113
}
114+
telemetry.RecordTEvent(context.Background(), telemetrydata.MakeTEvent("debug:panic", telemetrydata.TEventProps{
115+
PanicType: panicName,
116+
}))
110117
}
111118

112119
func sendTelemetryWrapper() {
113120
defer func() {
114121
panichandler.PanicHandler("sendTelemetryWrapper", recover())
115122
}()
116-
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
123+
ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second)
117124
defer cancelFn()
118125
beforeSendActivityUpdate(ctx)
119126
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
120127
if err != nil {
121128
log.Printf("[error] getting client data for telemetry: %v\n", err)
122129
return
123130
}
124-
err = wcloud.SendTelemetry(ctx, client.OID)
131+
err = wcloud.SendAllTelemetry(ctx, client.OID)
125132
if err != nil {
126133
log.Printf("[error] sending telemetry: %v\n", err)
127134
}
128135
}
129136

137+
func updateTelemetryCounts(lastCounts telemetrydata.TEventProps) telemetrydata.TEventProps {
138+
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
139+
defer cancelFn()
140+
var props telemetrydata.TEventProps
141+
props.CountBlocks, _ = wstore.DBGetCount[*waveobj.Block](ctx)
142+
props.CountTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
143+
props.CountWindows, _ = wstore.DBGetCount[*waveobj.Window](ctx)
144+
props.CountWorkspaces, _, _ = wstore.DBGetWSCounts(ctx)
145+
props.CountSSHConn = conncontroller.GetNumSSHHasConnected()
146+
props.CountWSLConn = wslconn.GetNumWSLHasConnected()
147+
props.CountViews, _ = wstore.DBGetBlockViewCounts(ctx)
148+
if utilfn.CompareAsMarshaledJson(props, lastCounts) {
149+
return lastCounts
150+
}
151+
tevent := telemetrydata.MakeTEvent("app:counts", props)
152+
err := telemetry.RecordTEvent(ctx, tevent)
153+
if err != nil {
154+
log.Printf("error recording counts tevent: %v\n", err)
155+
}
156+
return props
157+
}
158+
159+
func updateTelemetryCountsLoop() {
160+
defer func() {
161+
panichandler.PanicHandler("updateTelemetryCountsLoop", recover())
162+
}()
163+
var nextSend int64
164+
var lastCounts telemetrydata.TEventProps
165+
time.Sleep(TelemetryInitialCountsWait)
166+
for {
167+
if time.Now().Unix() > nextSend {
168+
nextSend = time.Now().Add(TelemetryCountsInterval).Unix()
169+
lastCounts = updateTelemetryCounts(lastCounts)
170+
}
171+
time.Sleep(TelemetryTick)
172+
}
173+
}
174+
130175
func beforeSendActivityUpdate(ctx context.Context) {
131176
activity := wshrpc.ActivityUpdate{}
132177
activity.NumTabs, _ = wstore.DBGetCount[*waveobj.Tab](ctx)
@@ -150,6 +195,26 @@ func startupActivityUpdate() {
150195
if err != nil {
151196
log.Printf("error updating startup activity: %v\n", err)
152197
}
198+
autoUpdateChannel := telemetry.AutoUpdateChannel()
199+
autoUpdateEnabled := telemetry.IsAutoUpdateEnabled()
200+
tevent := telemetrydata.MakeTEvent("app:startup", telemetrydata.TEventProps{
201+
UserSet: &telemetrydata.TEventUserProps{
202+
ClientVersion: "v" + WaveVersion,
203+
ClientBuildTime: BuildTime,
204+
ClientArch: wavebase.ClientArch(),
205+
ClientOSRelease: wavebase.UnameKernelRelease(),
206+
ClientIsDev: wavebase.IsDevMode(),
207+
AutoUpdateChannel: autoUpdateChannel,
208+
AutoUpdateEnabled: autoUpdateEnabled,
209+
},
210+
UserSetOnce: &telemetrydata.TEventUserProps{
211+
ClientInitialVersion: "v" + WaveVersion,
212+
},
213+
})
214+
err = telemetry.RecordTEvent(ctx, tevent)
215+
if err != nil {
216+
log.Printf("error recording startup event: %v\n", err)
217+
}
153218
}
154219

155220
func shutdownActivityUpdate() {
@@ -160,6 +225,15 @@ func shutdownActivityUpdate() {
160225
if err != nil {
161226
log.Printf("error updating shutdown activity: %v\n", err)
162227
}
228+
err = telemetry.TruncateActivityTEventForShutdown(ctx)
229+
if err != nil {
230+
log.Printf("error truncating activity t-event for shutdown: %v\n", err)
231+
}
232+
tevent := telemetrydata.MakeTEvent("app:shutdown", telemetrydata.TEventProps{})
233+
err = telemetry.RecordTEvent(ctx, tevent)
234+
if err != nil {
235+
log.Printf("error recording shutdown event: %v\n", err)
236+
}
163237
}
164238

165239
func createMainWshClient() {
@@ -283,15 +357,15 @@ func main() {
283357
}
284358

285359
createMainWshClient()
286-
287360
sigutil.InstallShutdownSignalHandlers(doShutdown)
288361
sigutil.InstallSIGUSR1Handler()
289-
290-
startupActivityUpdate()
362+
startConfigWatcher()
291363
go stdinReadWatch()
292364
go telemetryLoop()
293-
configWatcher()
365+
go updateTelemetryCountsLoop()
366+
startupActivityUpdate() // must be after startConfigWatcher()
294367
blocklogger.InitBlockLogger()
368+
295369
webListener, err := web.MakeTCPListener("web")
296370
if err != nil {
297371
log.Printf("error creating web listener: %v\n", err)

cmd/wsh/cmd/wshcmd-debug.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,24 @@ var debugBlockIdsCmd = &cobra.Command{
2424
Hidden: true,
2525
}
2626

27+
var debugSendTelemetryCmd = &cobra.Command{
28+
Use: "send-telemetry",
29+
Short: "send telemetry",
30+
RunE: debugSendTelemetryRun,
31+
Hidden: true,
32+
}
33+
2734
func init() {
2835
debugCmd.AddCommand(debugBlockIdsCmd)
36+
debugCmd.AddCommand(debugSendTelemetryCmd)
2937
rootCmd.AddCommand(debugCmd)
3038
}
3139

40+
func debugSendTelemetryRun(cmd *cobra.Command, args []string) error {
41+
err := wshclient.SendTelemetryCommand(RpcClient, nil)
42+
return err
43+
}
44+
3245
func debugBlockIdsRun(cmd *cobra.Command, args []string) error {
3346
oref, err := resolveBlockArg()
3447
if err != nil {

cmd/wsh/cmd/wshcmd-token.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ func init() {
2222
}
2323

2424
func tokenCmdRun(cmd *cobra.Command, args []string) (rtnErr error) {
25-
defer func() {
26-
sendActivity("token", rtnErr == nil)
27-
}()
2825
if len(args) != 2 {
2926
OutputHelpMessage(cmd)
3027
return fmt.Errorf("wsh token requires exactly 2 arguments, got %d", len(args))
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE db_tevent;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CREATE TABLE db_tevent (
2+
uuid varchar(36) PRIMARY KEY,
3+
ts int NOT NULL,
4+
tslocal varchar(100) NOT NULL,
5+
event varchar(50) NOT NULL,
6+
props json NOT NULL,
7+
uploaded boolean NOT NULL DEFAULT 0
8+
);

docs/docs/telemetry.mdx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ Lastly, some data is sent along with the telemetry that describes how to classif
9696
| AutoUpdateChannel | The type of auto update in use. This specifically refers to whether a latest or beta channel is selected. |
9797
| CurDay | The current day (in your time zone) when telemetry is sent. It does not include the time of day. |
9898

99+
## Geo Data
100+
101+
We do not store IP addresses in our telemetry table. However, CloudFlare passes us Geo-Location headers. We store these two header values:
102+
103+
| Name | Description |
104+
| ------------ | ----------------------------------------------------------------- |
105+
| CFCountry | 2-letter country code (e.g. "US", "FR", or "JP") |
106+
| CFRegionCode | region code (often a provence, region, or state within a country) |
107+
99108
---
100109

101110
## When Telemetry is Turned Off

emain/emain.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,31 @@ function getActivityDisplays(): ActivityDisplayType[] {
459459
return rtn;
460460
}
461461

462+
async function sendDisplaysTDataEvent() {
463+
const displays = getActivityDisplays();
464+
if (displays.length === 0) {
465+
return;
466+
}
467+
const props: TEventProps = {};
468+
props["display:count"] = displays.length;
469+
props["display:height"] = displays[0].height;
470+
props["display:width"] = displays[0].width;
471+
props["display:dpr"] = displays[0].dpr;
472+
props["display:all"] = displays;
473+
try {
474+
await RpcApi.RecordTEventCommand(
475+
ElectronWshClient,
476+
{
477+
event: "app:display",
478+
props,
479+
},
480+
{ noresponse: true }
481+
);
482+
} catch (e) {
483+
console.log("error sending display tdata event", e);
484+
}
485+
}
486+
462487
function logActiveState() {
463488
fireAndForget(async () => {
464489
const astate = getActivityState();
@@ -472,6 +497,18 @@ function logActiveState() {
472497
activity.displays = getActivityDisplays();
473498
try {
474499
await RpcApi.ActivityCommand(ElectronWshClient, activity, { noresponse: true });
500+
await RpcApi.RecordTEventCommand(
501+
ElectronWshClient,
502+
{
503+
event: "app:activity",
504+
props: {
505+
"activity:activeminutes": activity.activeminutes,
506+
"activity:fgminutes": activity.fgminutes,
507+
"activity:openminutes": activity.openminutes,
508+
},
509+
},
510+
{ noresponse: true }
511+
);
475512
} catch (e) {
476513
console.log("error logging active state", e);
477514
} finally {
@@ -621,6 +658,7 @@ async function appMain() {
621658
await relaunchBrowserWindows();
622659
await initDocsite();
623660
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
661+
setTimeout(sendDisplaysTDataEvent, 5000);
624662

625663
makeAppMenu();
626664
makeDockTaskbar();

frontend/app/block/blockframe.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
getConnStatusAtom,
1313
getSettingsKeyAtom,
1414
globalStore,
15+
recordTEvent,
1516
useBlockAtom,
1617
WOS,
1718
} from "@/app/store/global";
@@ -182,6 +183,7 @@ const BlockFrame_Header = ({
182183
return;
183184
}
184185
RpcApi.ActivityCommand(TabRpcClient, { nummagnify: 1 });
186+
recordTEvent("action:magnify", { "block:view": viewName });
185187
}, [magnified]);
186188

187189
if (blockData?.meta?.["frame:title"]) {

frontend/app/store/global.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { RpcApi } from "@/app/store/wshclientapi";
5+
import { TabRpcClient } from "@/app/store/wshrpcutil";
46
import {
57
getLayoutModelForTabById,
68
LayoutTreeActionType,
@@ -667,6 +669,13 @@ function setActiveTab(tabId: string) {
667669
getApi().setActiveTab(tabId);
668670
}
669671

672+
function recordTEvent(event: string, props?: TEventProps) {
673+
if (props == null) {
674+
props = {};
675+
}
676+
RpcApi.RecordTEventCommand(TabRpcClient, { event, props }, { noresponse: true });
677+
}
678+
670679
export {
671680
atoms,
672681
counterInc,
@@ -695,6 +704,7 @@ export {
695704
PLATFORM,
696705
pushFlashError,
697706
pushNotification,
707+
recordTEvent,
698708
refocusNode,
699709
registerBlockComponentModel,
700710
removeFlashError,

0 commit comments

Comments
 (0)