Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new tevents analytics framework #1894

Merged
merged 24 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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