Skip to content

Commit

Permalink
new tevents analytics framework (#1894)
Browse files Browse the repository at this point in the history
  • Loading branch information
sawka authored Feb 3, 2025
1 parent 8febd09 commit d7a9006
Show file tree
Hide file tree
Showing 32 changed files with 909 additions and 41 deletions.
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Targeting 1/31/25

## v0.12

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

- 🔷 Import/Export Tab Layouts and Widgets
- 🔷 log viewer
Expand Down
8 changes: 4 additions & 4 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ tasks:
- docsite:build:embedded
- build:backend
env:
WCLOUD_ENDPOINT: "https://ot2e112zx5.execute-api.us-west-2.amazonaws.com/dev"
WCLOUD_WS_ENDPOINT: "wss://5lfzlg5crl.execute-api.us-west-2.amazonaws.com/dev/"
WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central"
WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev/"

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

storybook:
desc: Start the Storybook server.
Expand Down
1 change: 1 addition & 0 deletions cmd/generatego/main-generatego.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func GenerateWshClient() error {
fmt.Fprintf(os.Stderr, "generating wshclient file to %s\n", WshClientFileName)
var buf strings.Builder
gogen.GenerateBoilerplate(&buf, "wshclient", []string{
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata",
"github.com/wavetermdev/waveterm/pkg/wshutil",
"github.com/wavetermdev/waveterm/pkg/wshrpc",
"github.com/wavetermdev/waveterm/pkg/wconfig",
Expand Down
90 changes: 82 additions & 8 deletions cmd/server/main-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import (
"github.com/wavetermdev/waveterm/pkg/remote/fileshare/wshfs"
"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/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"
Expand All @@ -46,6 +48,8 @@ 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

var shutdownOnce sync.Once

Expand Down Expand Up @@ -82,7 +86,7 @@ func stdinReadWatch() {
}
}

func configWatcher() {
func startConfigWatcher() {
watcher := wconfig.GetWatcher()
if watcher != nil {
watcher.Start()
Expand All @@ -101,32 +105,73 @@ func telemetryLoop() {
}
}

func panicTelemetryHandler() {
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(), 5*time.Second)
ctx, cancelFn := context.WithTimeout(context.Background(), 10*time.Second)
defer cancelFn()
beforeSendActivityUpdate(ctx)
client, err := wstore.DBGetSingleton[*waveobj.Client](ctx)
if err != nil {
log.Printf("[error] getting client data for telemetry: %v\n", err)
return
}
err = wcloud.SendTelemetry(ctx, client.OID)
err = wcloud.SendAllTelemetry(ctx, client.OID)
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.CountViews, _ = wstore.DBGetBlockViewCounts(ctx)
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)
Expand All @@ -150,6 +195,26 @@ func startupActivityUpdate() {
if err != nil {
log.Printf("error updating startup activity: %v\n", err)
}
autoUpdateChannel := telemetry.AutoUpdateChannel()
autoUpdateEnabled := telemetry.IsAutoUpdateEnabled()
tevent := telemetrydata.MakeTEvent("app:startup", telemetrydata.TEventProps{
UserSet: &telemetrydata.TEventUserProps{
ClientVersion: "v" + WaveVersion,
ClientBuildTime: BuildTime,
ClientArch: wavebase.ClientArch(),
ClientOSRelease: wavebase.UnameKernelRelease(),
ClientIsDev: wavebase.IsDevMode(),
AutoUpdateChannel: autoUpdateChannel,
AutoUpdateEnabled: autoUpdateEnabled,
},
UserSetOnce: &telemetrydata.TEventUserProps{
ClientInitialVersion: "v" + WaveVersion,
},
})
err = telemetry.RecordTEvent(ctx, tevent)
if err != nil {
log.Printf("error recording startup event: %v\n", err)
}
}

func shutdownActivityUpdate() {
Expand All @@ -160,6 +225,15 @@ func shutdownActivityUpdate() {
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() {
Expand Down Expand Up @@ -283,15 +357,15 @@ func main() {
}

createMainWshClient()

sigutil.InstallShutdownSignalHandlers(doShutdown)
sigutil.InstallSIGUSR1Handler()

startupActivityUpdate()
startConfigWatcher()
go stdinReadWatch()
go telemetryLoop()
configWatcher()
go updateTelemetryCountsLoop()
startupActivityUpdate() // must be after startConfigWatcher()
blocklogger.InitBlockLogger()

webListener, err := web.MakeTCPListener("web")
if err != nil {
log.Printf("error creating web listener: %v\n", err)
Expand Down
13 changes: 13 additions & 0 deletions cmd/wsh/cmd/wshcmd-debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,24 @@ 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 {
Expand Down
3 changes: 0 additions & 3 deletions cmd/wsh/cmd/wshcmd-token.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ func init() {
}

func tokenCmdRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("token", rtnErr == nil)
}()
if len(args) != 2 {
OutputHelpMessage(cmd)
return fmt.Errorf("wsh token requires exactly 2 arguments, got %d", len(args))
Expand Down
1 change: 1 addition & 0 deletions db/migrations-wstore/000007_events.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE db_tevent;
8 changes: 8 additions & 0 deletions db/migrations-wstore/000007_events.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE db_tevent (
uuid varchar(36) PRIMARY KEY,
ts int NOT NULL,
tslocal varchar(100) NOT NULL,
event varchar(50) NOT NULL,
props json NOT NULL,
uploaded boolean NOT NULL DEFAULT 0
);
9 changes: 9 additions & 0 deletions docs/docs/telemetry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,15 @@ Lastly, some data is sent along with the telemetry that describes how to classif
| 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
Expand Down
38 changes: 38 additions & 0 deletions emain/emain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,31 @@ function getActivityDisplays(): ActivityDisplayType[] {
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();
Expand All @@ -472,6 +497,18 @@ function logActiveState() {
activity.displays = getActivityDisplays();
try {
await RpcApi.ActivityCommand(ElectronWshClient, activity, { noresponse: true });
await RpcApi.RecordTEventCommand(
ElectronWshClient,
{
event: "app:activity",
props: {
"activity:activeminutes": activity.activeminutes,
"activity:fgminutes": activity.fgminutes,
"activity:openminutes": activity.openminutes,
},
},
{ noresponse: true }
);
} catch (e) {
console.log("error logging active state", e);
} finally {
Expand Down Expand Up @@ -621,6 +658,7 @@ async function appMain() {
await relaunchBrowserWindows();
await initDocsite();
setTimeout(runActiveTimer, 5000); // start active timer, wait 5s just to be safe
setTimeout(sendDisplaysTDataEvent, 5000);

makeAppMenu();
makeDockTaskbar();
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getConnStatusAtom,
getSettingsKeyAtom,
globalStore,
recordTEvent,
useBlockAtom,
WOS,
} from "@/app/store/global";
Expand Down Expand Up @@ -182,6 +183,7 @@ const BlockFrame_Header = ({
return;
}
RpcApi.ActivityCommand(TabRpcClient, { nummagnify: 1 });
recordTEvent("action:magnify", { "block:view": viewName });
}, [magnified]);

if (blockData?.meta?.["frame:title"]) {
Expand Down
10 changes: 10 additions & 0 deletions frontend/app/store/global.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import {
getLayoutModelForTabById,
LayoutTreeActionType,
Expand Down Expand Up @@ -667,6 +669,13 @@ function setActiveTab(tabId: string) {
getApi().setActiveTab(tabId);
}

function recordTEvent(event: string, props?: TEventProps) {
if (props == null) {
props = {};
}
RpcApi.RecordTEventCommand(TabRpcClient, { event, props }, { noresponse: true });
}

export {
atoms,
counterInc,
Expand Down Expand Up @@ -695,6 +704,7 @@ export {
PLATFORM,
pushFlashError,
pushNotification,
recordTEvent,
refocusNode,
registerBlockComponentModel,
removeFlashError,
Expand Down
Loading

0 comments on commit d7a9006

Please sign in to comment.