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 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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export {
|
||||||
IssuesUpdatedEvent,
|
IssuesUpdatedEvent,
|
||||||
SlowCompletionResponseTimeIssue,
|
SlowCompletionResponseTimeIssue,
|
||||||
HighCompletionTimeoutRateIssue,
|
HighCompletionTimeoutRateIssue,
|
||||||
|
ClientProperties,
|
||||||
AgentInitOptions,
|
AgentInitOptions,
|
||||||
ServerHealthState,
|
ServerHealthState,
|
||||||
CompletionRequest,
|
CompletionRequest,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue