feat(agent): add user properties for anonymous usage tracking. (#483)
parent
ff4030799a
commit
50ac1ced0a
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export {
|
|||
IssuesUpdatedEvent,
|
||||
SlowCompletionResponseTimeIssue,
|
||||
HighCompletionTimeoutRateIssue,
|
||||
ClientProperties,
|
||||
AgentInitOptions,
|
||||
ServerHealthState,
|
||||
CompletionRequest,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue