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 type { components as ApiComponents } from "./types/tabbyApi";
import { AgentConfig, PartialAgentConfig } from "./AgentConfig"; import { AgentConfig, PartialAgentConfig } from "./AgentConfig";
export type ClientProperties = Partial<{
user: Record<string, any>;
session: Record<string, any>;
}>;
export type AgentInitOptions = Partial<{ export type AgentInitOptions = Partial<{
config: PartialAgentConfig; config: PartialAgentConfig;
client: string; clientProperties: ClientProperties;
clientProperties: Record<string, any>;
}>; }>;
export type ServerHealthState = ApiComponents["schemas"]["HealthState"]; export type ServerHealthState = ApiComponents["schemas"]["HealthState"];
@ -60,6 +64,12 @@ export interface AgentFunction {
*/ */
finalize(): Promise<boolean>; 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: * The agent configuration has the following levels, will be deep merged in the order:
* 1. Default config * 1. Default config

View File

@ -1,6 +1,7 @@
import { name as agentName, version as agentVersion } from "../package.json"; import { name as agentName, version as agentVersion } from "../package.json";
import createClient from "openapi-fetch"; import createClient from "openapi-fetch";
import type { paths as CloudApi } from "./types/cloudApi"; import type { paths as CloudApi } from "./types/cloudApi";
import { setProperty } from "dot-prop";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import { isBrowser } from "./env"; import { isBrowser } from "./env";
import { rootLogger } from "./logger"; import { rootLogger } from "./logger";
@ -16,7 +17,8 @@ export class AnonymousUsageLogger {
? undefined ? undefined
: `${process.version} ${process.platform} ${require("os").arch()} ${require("os").release()}`, : `${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 emittedUniqueEvent: string[] = [];
private dataStore: DataStore | null = null; private dataStore: DataStore | null = null;
private anonymousId: string; private anonymousId: string;
@ -55,9 +57,18 @@ export class AnonymousUsageLogger {
} }
} }
addProperties(properties: { [key: string]: any }) { /**
// not a deep merge * Set properties to be sent with every event in this session.
this.properties = { ...this.properties, ...properties }; */
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 } = {}) { async uniqueEvent(event: string, data: { [key: string]: any } = {}) {
@ -74,16 +85,21 @@ export class AnonymousUsageLogger {
if (unique) { if (unique) {
this.emittedUniqueEvent.push(event); 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 { try {
await this.anonymousUsageTrackingApi.POST("/usage", { await this.anonymousUsageTrackingApi.POST("/usage", {
body: { body: {
distinctId: this.anonymousId, distinctId: this.anonymousId,
event, event,
properties: { properties,
...this.systemData,
...this.properties,
...data,
},
}, },
}); });
} catch (error) { } catch (error) {

View File

@ -11,6 +11,7 @@ import type {
AgentStatus, AgentStatus,
AgentIssue, AgentIssue,
AgentEvent, AgentEvent,
ClientProperties,
AgentInitOptions, AgentInitOptions,
AbortSignalOption, AbortSignalOption,
ServerHealthState, ServerHealthState,
@ -262,11 +263,19 @@ export class TabbyAgent extends EventEmitter implements Agent {
} }
public async initialize(options: AgentInitOptions): Promise<boolean> { public async initialize(options: AgentInitOptions): Promise<boolean> {
if (options.client || options.clientProperties) { if (options.clientProperties) {
// Client info is only used in logging for now const { user: userProp, session: sessionProp } = options.clientProperties;
// `pino.Logger.setBindings` is not present in the browser allLoggers.forEach((logger) => logger.setBindings?.({ ...sessionProp }));
allLoggers.forEach((logger) => logger.setBindings?.({ client: options.client, ...options.clientProperties })); if (sessionProp) {
this.anonymousUsageLogger.addProperties({ client: options.client, ...options.clientProperties }); 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) { if (userAgentConfig) {
await userAgentConfig.load(); await userAgentConfig.load();
@ -301,6 +310,21 @@ export class TabbyAgent extends EventEmitter implements Agent {
return true; 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> { public async updateConfig(key: string, value: any): Promise<boolean> {
const current = getProperty(this.clientConfig, key); const current = getProperty(this.clientConfig, key);
if (!deepEqual(current, value)) { if (!deepEqual(current, value)) {

View File

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

View File

@ -1,7 +1,7 @@
import { ExtensionContext, workspace, env, version } from "vscode"; 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 configuration = workspace.getConfiguration("tabby");
const config: PartialAgentConfig = {}; const config: PartialAgentConfig = {};
const endpoint = configuration.get<string>("api.endpoint"); const endpoint = configuration.get<string>("api.endpoint");
@ -14,7 +14,27 @@ function getWorkspaceConfiguration(): PartialAgentConfig {
config.anonymousUsageTracking = { config.anonymousUsageTracking = {
disable: anonymousUsageTrackingDisabled, 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; 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 agent = await TabbyAgent.create({ dataStore: env.appHost === "desktop" ? undefined : extensionDataStore });
const initPromise = agent.initialize({ const initPromise = agent.initialize(buildInitOptions(context));
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,
},
},
});
workspace.onDidChangeConfiguration(async (event) => { workspace.onDidChangeConfiguration(async (event) => {
await initPromise; await initPromise;
const configuration = workspace.getConfiguration("tabby"); 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); const anonymousUsageTrackingDisabled = configuration.get<boolean>("usage.anonymousUsageTracking", false);
agent.updateConfig("anonymousUsageTracking.disable", anonymousUsageTrackingDisabled); 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; instance = agent;
} }