From 50ac1ced0a0015e2a89e12a8d7d042b79f9b494c Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Thu, 28 Sep 2023 10:15:39 +0800 Subject: [PATCH] feat(agent): add user properties for anonymous usage tracking. (#483) --- clients/tabby-agent/src/Agent.ts | 14 +++++- .../tabby-agent/src/AnonymousUsageLogger.ts | 34 +++++++++---- clients/tabby-agent/src/TabbyAgent.ts | 34 +++++++++++-- clients/tabby-agent/src/index.ts | 1 + clients/vscode/src/agent.ts | 49 ++++++++++++------- 5 files changed, 99 insertions(+), 33 deletions(-) diff --git a/clients/tabby-agent/src/Agent.ts b/clients/tabby-agent/src/Agent.ts index 6d5d9fa..e2c7b69 100644 --- a/clients/tabby-agent/src/Agent.ts +++ b/clients/tabby-agent/src/Agent.ts @@ -1,10 +1,14 @@ import type { components as ApiComponents } from "./types/tabbyApi"; import { AgentConfig, PartialAgentConfig } from "./AgentConfig"; +export type ClientProperties = Partial<{ + user: Record; + session: Record; +}>; + export type AgentInitOptions = Partial<{ config: PartialAgentConfig; - client: string; - clientProperties: Record; + clientProperties: ClientProperties; }>; export type ServerHealthState = ApiComponents["schemas"]["HealthState"]; @@ -60,6 +64,12 @@ export interface AgentFunction { */ finalize(): Promise; + /** + * Update client properties. + * Client properties are mostly used for logging and anonymous usage statistics. + */ + updateClientProperties(type: keyof ClientProperties, key: string, value: any): Promise; + /** * The agent configuration has the following levels, will be deep merged in the order: * 1. Default config diff --git a/clients/tabby-agent/src/AnonymousUsageLogger.ts b/clients/tabby-agent/src/AnonymousUsageLogger.ts index a5b4514..8e77303 100644 --- a/clients/tabby-agent/src/AnonymousUsageLogger.ts +++ b/clients/tabby-agent/src/AnonymousUsageLogger.ts @@ -1,6 +1,7 @@ import { name as agentName, version as agentVersion } from "../package.json"; import createClient from "openapi-fetch"; import type { paths as CloudApi } from "./types/cloudApi"; +import { setProperty } from "dot-prop"; import { v4 as uuid } from "uuid"; import { isBrowser } from "./env"; import { rootLogger } from "./logger"; @@ -16,7 +17,8 @@ export class AnonymousUsageLogger { ? undefined : `${process.version} ${process.platform} ${require("os").arch()} ${require("os").release()}`, }; - private properties: { [key: string]: any } = {}; + private sessionProperties: Record = {}; + private pendingUserProperties: Record = {}; private emittedUniqueEvent: string[] = []; private dataStore: DataStore | null = null; private anonymousId: string; @@ -55,9 +57,18 @@ export class AnonymousUsageLogger { } } - addProperties(properties: { [key: string]: any }) { - // not a deep merge - this.properties = { ...this.properties, ...properties }; + /** + * Set properties to be sent with every event in this session. + */ + setSessionProperties(key: string, value: any) { + setProperty(this.sessionProperties, key, value); + } + + /** + * Set properties which will be bind to the user. + */ + setUserProperties(key: string, value: any) { + setProperty(this.pendingUserProperties, key, value); } async uniqueEvent(event: string, data: { [key: string]: any } = {}) { @@ -74,16 +85,21 @@ export class AnonymousUsageLogger { if (unique) { this.emittedUniqueEvent.push(event); } + const properties = { + ...this.systemData, + ...this.sessionProperties, + ...data, + }; + if (Object.keys(this.pendingUserProperties).length > 0) { + properties["$set"] = this.pendingUserProperties; + this.pendingUserProperties = {}; + } try { await this.anonymousUsageTrackingApi.POST("/usage", { body: { distinctId: this.anonymousId, event, - properties: { - ...this.systemData, - ...this.properties, - ...data, - }, + properties, }, }); } catch (error) { diff --git a/clients/tabby-agent/src/TabbyAgent.ts b/clients/tabby-agent/src/TabbyAgent.ts index ea121e0..8cb7f99 100644 --- a/clients/tabby-agent/src/TabbyAgent.ts +++ b/clients/tabby-agent/src/TabbyAgent.ts @@ -11,6 +11,7 @@ import type { AgentStatus, AgentIssue, AgentEvent, + ClientProperties, AgentInitOptions, AbortSignalOption, ServerHealthState, @@ -262,11 +263,19 @@ export class TabbyAgent extends EventEmitter implements Agent { } public async initialize(options: AgentInitOptions): Promise { - if (options.client || options.clientProperties) { - // Client info is only used in logging for now - // `pino.Logger.setBindings` is not present in the browser - allLoggers.forEach((logger) => logger.setBindings?.({ client: options.client, ...options.clientProperties })); - this.anonymousUsageLogger.addProperties({ client: options.client, ...options.clientProperties }); + if (options.clientProperties) { + const { user: userProp, session: sessionProp } = options.clientProperties; + allLoggers.forEach((logger) => logger.setBindings?.({ ...sessionProp })); + if (sessionProp) { + Object.entries(sessionProp).forEach(([key, value]) => { + this.anonymousUsageLogger.setSessionProperties(key, value); + }); + } + if (userProp) { + Object.entries(userProp).forEach(([key, value]) => { + this.anonymousUsageLogger.setUserProperties(key, value); + }); + } } if (userAgentConfig) { await userAgentConfig.load(); @@ -301,6 +310,21 @@ export class TabbyAgent extends EventEmitter implements Agent { return true; } + public async updateClientProperties(type: keyof ClientProperties, key: string, value: any): Promise { + switch (type) { + case "session": + const prop = {}; + setProperty(prop, key, value); + allLoggers.forEach((logger) => logger.setBindings?.(prop)); + this.anonymousUsageLogger.setSessionProperties(key, value); + break; + case "user": + this.anonymousUsageLogger.setUserProperties(key, value); + break; + } + return true; + } + public async updateConfig(key: string, value: any): Promise { const current = getProperty(this.clientConfig, key); if (!deepEqual(current, value)) { diff --git a/clients/tabby-agent/src/index.ts b/clients/tabby-agent/src/index.ts index c714a98..36257ab 100644 --- a/clients/tabby-agent/src/index.ts +++ b/clients/tabby-agent/src/index.ts @@ -12,6 +12,7 @@ export { IssuesUpdatedEvent, SlowCompletionResponseTimeIssue, HighCompletionTimeoutRateIssue, + ClientProperties, AgentInitOptions, ServerHealthState, CompletionRequest, diff --git a/clients/vscode/src/agent.ts b/clients/vscode/src/agent.ts index 12f0d21..9eed318 100644 --- a/clients/vscode/src/agent.ts +++ b/clients/vscode/src/agent.ts @@ -1,7 +1,7 @@ import { ExtensionContext, workspace, env, version } from "vscode"; -import { TabbyAgent, PartialAgentConfig, DataStore } from "tabby-agent"; +import { TabbyAgent, AgentInitOptions, PartialAgentConfig, ClientProperties, DataStore } from "tabby-agent"; -function getWorkspaceConfiguration(): PartialAgentConfig { +function buildInitOptions(context: ExtensionContext): AgentInitOptions { const configuration = workspace.getConfiguration("tabby"); const config: PartialAgentConfig = {}; const endpoint = configuration.get("api.endpoint"); @@ -14,7 +14,27 @@ function getWorkspaceConfiguration(): PartialAgentConfig { config.anonymousUsageTracking = { disable: anonymousUsageTrackingDisabled, }; - return config; + const clientProperties: ClientProperties = { + user: { + vscode: { + triggerMode: configuration.get("inlineCompletion.triggerMode", "automatic"), + keybindings: configuration.get("keybindings", "vscode-style"), + }, + }, + session: { + client: `${env.appName} ${env.appHost} ${version}, ${context.extension.id} ${context.extension.packageJSON.version}`, + ide: { + name: `${env.appName} ${env.appHost}`, + version: version, + }, + tabby_plugin: { + name: context.extension.id, + version: context.extension.packageJSON.version, + }, + }, + }; + + return { config, clientProperties }; } var instance: TabbyAgent | undefined = undefined; @@ -38,20 +58,7 @@ export async function createAgentInstance(context: ExtensionContext): Promise { await initPromise; const configuration = workspace.getConfiguration("tabby"); @@ -67,6 +74,14 @@ export async function createAgentInstance(context: ExtensionContext): Promise("usage.anonymousUsageTracking", false); agent.updateConfig("anonymousUsageTracking.disable", anonymousUsageTrackingDisabled); } + if (event.affectsConfiguration("tabby.inlineCompletion.triggerMode")) { + const triggerMode = configuration.get("inlineCompletion.triggerMode", "automatic"); + agent.updateClientProperties("user", "vscode.triggerMode", triggerMode); + } + if (event.affectsConfiguration("tabby.keybindings")) { + const keybindings = configuration.get("keybindings", "vscode-style"); + agent.updateClientProperties("user", "vscode.keybindings", keybindings); + } }); instance = agent; }