feat(agent): add user properties for anonymous usage tracking. (#483)

release-0.2
Zhiming Ma 2023-09-28 10:15:39 +08:00 committed by GitHub
parent ff4030799a
commit 50ac1ced0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 99 additions and 33 deletions

View File

@ -1,10 +1,14 @@
import type { components as ApiComponents } from "./types/tabbyApi";
import { AgentConfig, PartialAgentConfig } from "./AgentConfig";
export type ClientProperties = Partial<{
user: Record<string, any>;
session: Record<string, any>;
}>;
export type AgentInitOptions = Partial<{
config: PartialAgentConfig;
client: string;
clientProperties: Record<string, any>;
clientProperties: ClientProperties;
}>;
export type ServerHealthState = ApiComponents["schemas"]["HealthState"];
@ -60,6 +64,12 @@ export interface AgentFunction {
*/
finalize(): Promise<boolean>;
/**
* Update client properties.
* Client properties are mostly used for logging and anonymous usage statistics.
*/
updateClientProperties(type: keyof ClientProperties, key: string, value: any): Promise<boolean>;
/**
* The agent configuration has the following levels, will be deep merged in the order:
* 1. Default config

View File

@ -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<string, any> = {};
private pendingUserProperties: Record<string, any> = {};
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) {

View File

@ -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<boolean> {
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<boolean> {
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<boolean> {
const current = getProperty(this.clientConfig, key);
if (!deepEqual(current, value)) {

View File

@ -12,6 +12,7 @@ export {
IssuesUpdatedEvent,
SlowCompletionResponseTimeIssue,
HighCompletionTimeoutRateIssue,
ClientProperties,
AgentInitOptions,
ServerHealthState,
CompletionRequest,

View File

@ -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<string>("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<Ta
},
};
const agent = await TabbyAgent.create({ dataStore: env.appHost === "desktop" ? undefined : extensionDataStore });
const initPromise = agent.initialize({
config: getWorkspaceConfiguration(),
client: `${env.appName} ${env.appHost} ${version}, ${context.extension.id} ${context.extension.packageJSON.version}`,
clientProperties: {
ide: {
name: `${env.appName} ${env.appHost}`,
version: version,
},
tabby_plugin: {
name: context.extension.id,
version: context.extension.packageJSON.version,
},
},
});
const initPromise = agent.initialize(buildInitOptions(context));
workspace.onDidChangeConfiguration(async (event) => {
await initPromise;
const configuration = workspace.getConfiguration("tabby");
@ -67,6 +74,14 @@ export async function createAgentInstance(context: ExtensionContext): Promise<Ta
const anonymousUsageTrackingDisabled = configuration.get<boolean>("usage.anonymousUsageTracking", false);
agent.updateConfig("anonymousUsageTracking.disable", anonymousUsageTrackingDisabled);
}
if (event.affectsConfiguration("tabby.inlineCompletion.triggerMode")) {
const triggerMode = configuration.get<string>("inlineCompletion.triggerMode", "automatic");
agent.updateClientProperties("user", "vscode.triggerMode", triggerMode);
}
if (event.affectsConfiguration("tabby.keybindings")) {
const keybindings = configuration.get<string>("keybindings", "vscode-style");
agent.updateClientProperties("user", "vscode.keybindings", keybindings);
}
});
instance = agent;
}