feat: Add anonymous usage tracking. (#243)
* feat: Add anonymous usage tracking. * fix: anonymous usage post body. * fix: anonymous usage tracking initialize agent event name. * chore: build tabby-agent. * fix: agent anonymous data store. * fix anonymous usage tracking event name: AgentInitialized.improve-workflow
parent
8ee700089f
commit
534d3d5ea7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -100,6 +100,7 @@ interface AgentEventEmitter {
|
|||
type Agent = AgentFunction & AgentEventEmitter;
|
||||
|
||||
type StoredData = {
|
||||
anonymousId: string;
|
||||
auth: {
|
||||
[endpoint: string]: {
|
||||
jwt: string;
|
||||
|
|
@ -121,6 +122,7 @@ type TabbyAgentOptions = {
|
|||
};
|
||||
declare class TabbyAgent extends EventEmitter implements Agent {
|
||||
private readonly logger;
|
||||
private anonymousUsageLogger;
|
||||
private config;
|
||||
private status;
|
||||
private api;
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,8 +5,8 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
for (var name2 in all)
|
||||
__defProp(target, name2, { get: all[name2], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
|
|
@ -55,7 +55,7 @@ module.exports = __toCommonJS(src_exports);
|
|||
|
||||
// src/TabbyAgent.ts
|
||||
var import_events2 = require("events");
|
||||
var import_uuid = require("uuid");
|
||||
var import_uuid2 = require("uuid");
|
||||
var import_deep_equal2 = __toESM(require("deep-equal"));
|
||||
var import_deepmerge = __toESM(require("deepmerge"));
|
||||
|
||||
|
|
@ -218,15 +218,15 @@ var getQueryString = (params) => {
|
|||
const append = (key, value) => {
|
||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
};
|
||||
const process = (key, value) => {
|
||||
const process2 = (key, value) => {
|
||||
if (isDefined(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => {
|
||||
process(key, v);
|
||||
process2(key, v);
|
||||
});
|
||||
} else if (typeof value === "object") {
|
||||
Object.entries(value).forEach(([k, v]) => {
|
||||
process(`${key}[${k}]`, v);
|
||||
process2(`${key}[${k}]`, v);
|
||||
});
|
||||
} else {
|
||||
append(key, value);
|
||||
|
|
@ -234,7 +234,7 @@ var getQueryString = (params) => {
|
|||
}
|
||||
};
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
process(key, value);
|
||||
process2(key, value);
|
||||
});
|
||||
if (qs.length > 0) {
|
||||
return `?${qs.join("&")}`;
|
||||
|
|
@ -258,7 +258,7 @@ var getUrl = (config, options) => {
|
|||
var getFormData = (options) => {
|
||||
if (options.formData) {
|
||||
const formData = new import_form_data.default();
|
||||
const process = (key, value) => {
|
||||
const process2 = (key, value) => {
|
||||
if (isString(value) || isBlob(value)) {
|
||||
formData.append(key, value);
|
||||
} else {
|
||||
|
|
@ -267,9 +267,9 @@ var getFormData = (options) => {
|
|||
};
|
||||
Object.entries(options.formData).filter(([_, value]) => isDefined(value)).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((v) => process(key, v));
|
||||
value.forEach((v) => process2(key, v));
|
||||
} else {
|
||||
process(key, value);
|
||||
process2(key, value);
|
||||
}
|
||||
});
|
||||
return formData;
|
||||
|
|
@ -489,6 +489,7 @@ var TabbyApi = class {
|
|||
};
|
||||
|
||||
// src/utils.ts
|
||||
var isBrowser = false;
|
||||
function splitLines(input) {
|
||||
return input.match(/.*(?:$|\r?\n)/g).filter(Boolean);
|
||||
}
|
||||
|
|
@ -541,6 +542,16 @@ var ApiService = class {
|
|||
query
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param body object for anonymous usage tracking
|
||||
*/
|
||||
usage(body) {
|
||||
return this.httpRequest.request({
|
||||
method: "POST",
|
||||
url: "/usage",
|
||||
body
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// src/cloud/CloudApi.ts
|
||||
|
|
@ -562,7 +573,7 @@ var CloudApi = class {
|
|||
};
|
||||
|
||||
// src/dataStore.ts
|
||||
var dataStore = false ? null : (() => {
|
||||
var dataStore = isBrowser ? null : (() => {
|
||||
const dataFile = require("path").join(require("os").homedir(), ".tabby", "agent", "data.json");
|
||||
const fs = require("fs-extra");
|
||||
return {
|
||||
|
|
@ -578,7 +589,7 @@ var dataStore = false ? null : (() => {
|
|||
|
||||
// src/logger.ts
|
||||
var import_pino = __toESM(require("pino"));
|
||||
var stream = false ? null : (
|
||||
var stream = isBrowser ? null : (
|
||||
/**
|
||||
* Default rotating file locate at `~/.tabby/agent-logs/`.
|
||||
*/
|
||||
|
|
@ -884,6 +895,68 @@ async function postprocess(request2, response) {
|
|||
return new Promise((resolve2) => resolve2(response)).then(applyFilter(removeDuplicateLines(request2))).then(applyFilter(dropBlank));
|
||||
}
|
||||
|
||||
// package.json
|
||||
var name = "tabby-agent";
|
||||
var version = "0.0.1";
|
||||
|
||||
// src/AnonymousUsageLogger.ts
|
||||
var import_uuid = require("uuid");
|
||||
var AnonymousUsageLogger = class {
|
||||
constructor() {
|
||||
this.anonymousUsageTrackingApi = new CloudApi({ BASE: "https://app.tabbyml.com/api" });
|
||||
this.logger = rootLogger.child({ component: "AnonymousUsage" });
|
||||
this.systemData = {
|
||||
agent: `${name}, ${version}`,
|
||||
browser: isBrowser ? navigator?.userAgent || "browser" : void 0,
|
||||
node: isBrowser ? void 0 : `${process.version} ${process.platform} ${require("os").arch()} ${require("os").release()}`
|
||||
};
|
||||
this.dataStore = null;
|
||||
}
|
||||
static async create(options) {
|
||||
const logger2 = new AnonymousUsageLogger();
|
||||
logger2.dataStore = options.dataStore || dataStore;
|
||||
await logger2.checkAnonymousId();
|
||||
return logger2;
|
||||
}
|
||||
async checkAnonymousId() {
|
||||
if (this.dataStore) {
|
||||
try {
|
||||
await this.dataStore.load();
|
||||
} catch (error) {
|
||||
this.logger.debug({ error }, "Error when loading anonymousId");
|
||||
}
|
||||
if (typeof this.dataStore.data["anonymousId"] === "string") {
|
||||
this.anonymousId = this.dataStore.data["anonymousId"];
|
||||
} else {
|
||||
this.anonymousId = (0, import_uuid.v4)();
|
||||
this.dataStore.data["anonymousId"] = this.anonymousId;
|
||||
try {
|
||||
await this.dataStore.save();
|
||||
} catch (error) {
|
||||
this.logger.debug({ error }, "Error when saving anonymousId");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.anonymousId = (0, import_uuid.v4)();
|
||||
}
|
||||
}
|
||||
async event(event, data) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
await this.anonymousUsageTrackingApi.api.usage({
|
||||
distinctId: this.anonymousId,
|
||||
event,
|
||||
properties: {
|
||||
...this.systemData,
|
||||
...data
|
||||
}
|
||||
}).catch((error) => {
|
||||
this.logger.error({ error }, "Error when sending anonymous usage data");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// src/TabbyAgent.ts
|
||||
var _TabbyAgent = class extends import_events2.EventEmitter {
|
||||
constructor() {
|
||||
|
|
@ -905,11 +978,13 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
|
|||
static async create(options) {
|
||||
const agent = new _TabbyAgent();
|
||||
agent.dataStore = options?.dataStore;
|
||||
agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore });
|
||||
await agent.applyConfig();
|
||||
return agent;
|
||||
}
|
||||
async applyConfig() {
|
||||
allLoggers.forEach((logger2) => logger2.level = this.config.logs.level);
|
||||
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
|
||||
if (this.config.server.endpoint !== this.auth?.endpoint) {
|
||||
this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore });
|
||||
this.auth.on("updated", this.onAuthUpdated.bind(this));
|
||||
|
|
@ -978,6 +1053,9 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
|
|||
if (options.config) {
|
||||
await this.updateConfig(options.config);
|
||||
}
|
||||
await this.anonymousUsageLogger.event("AgentInitialized", {
|
||||
client: options.client
|
||||
});
|
||||
this.logger.debug({ options }, "Initialized");
|
||||
return this.status !== "notInitialized";
|
||||
}
|
||||
|
|
@ -1029,7 +1107,7 @@ var _TabbyAgent = class extends import_events2.EventEmitter {
|
|||
this.logger.debug("Segment prefix is blank, returning empty completion response");
|
||||
return new CancelablePromise((resolve2) => {
|
||||
resolve2({
|
||||
id: "agent-" + (0, import_uuid.v4)(),
|
||||
id: "agent-" + (0, import_uuid2.v4)(),
|
||||
choices: []
|
||||
});
|
||||
});
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,72 @@
|
|||
import { name as agentName, version as agentVersion } from "../package.json";
|
||||
import { CloudApi } from "./cloud";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { isBrowser } from "./utils";
|
||||
import { rootLogger } from "./logger";
|
||||
import { dataStore, DataStore } from "./dataStore";
|
||||
|
||||
export class AnonymousUsageLogger {
|
||||
private anonymousUsageTrackingApi = new CloudApi({ BASE: "https://app.tabbyml.com/api" });
|
||||
private logger = rootLogger.child({ component: "AnonymousUsage" });
|
||||
private systemData = {
|
||||
agent: `${agentName}, ${agentVersion}`,
|
||||
browser: isBrowser ? navigator?.userAgent || "browser" : undefined,
|
||||
node: isBrowser
|
||||
? undefined
|
||||
: `${process.version} ${process.platform} ${require("os").arch()} ${require("os").release()}`,
|
||||
};
|
||||
private dataStore: DataStore | null = null;
|
||||
private anonymousId: string;
|
||||
|
||||
disabled: boolean;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static async create(options: { dataStore: DataStore }): Promise<AnonymousUsageLogger> {
|
||||
const logger = new AnonymousUsageLogger();
|
||||
logger.dataStore = options.dataStore || dataStore;
|
||||
await logger.checkAnonymousId();
|
||||
return logger;
|
||||
}
|
||||
|
||||
private async checkAnonymousId() {
|
||||
if (this.dataStore) {
|
||||
try {
|
||||
await this.dataStore.load();
|
||||
} catch (error) {
|
||||
this.logger.debug({ error }, "Error when loading anonymousId");
|
||||
}
|
||||
if (typeof this.dataStore.data["anonymousId"] === "string") {
|
||||
this.anonymousId = this.dataStore.data["anonymousId"];
|
||||
} else {
|
||||
this.anonymousId = uuid();
|
||||
this.dataStore.data["anonymousId"] = this.anonymousId;
|
||||
try {
|
||||
await this.dataStore.save();
|
||||
} catch (error) {
|
||||
this.logger.debug({ error }, "Error when saving anonymousId");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.anonymousId = uuid();
|
||||
}
|
||||
}
|
||||
|
||||
async event(event: string, data: any) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
await this.anonymousUsageTrackingApi.api
|
||||
.usage({
|
||||
distinctId: this.anonymousId,
|
||||
event,
|
||||
properties: {
|
||||
...this.systemData,
|
||||
...data,
|
||||
},
|
||||
})
|
||||
.catch((error) => {
|
||||
this.logger.error({ error }, "Error when sending anonymous usage data");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import { CompletionCache } from "./CompletionCache";
|
|||
import { DataStore } from "./dataStore";
|
||||
import { postprocess } from "./postprocess";
|
||||
import { rootLogger, allLoggers } from "./logger";
|
||||
import { AnonymousUsageLogger } from "./AnonymousUsageLogger";
|
||||
|
||||
/**
|
||||
* Different from AgentInitOptions or AgentConfig, this may contain non-serializable objects,
|
||||
|
|
@ -30,6 +31,7 @@ export type TabbyAgentOptions = {
|
|||
|
||||
export class TabbyAgent extends EventEmitter implements Agent {
|
||||
private readonly logger = rootLogger.child({ component: "TabbyAgent" });
|
||||
private anonymousUsageLogger: AnonymousUsageLogger;
|
||||
private config: AgentConfig = defaultAgentConfig;
|
||||
private status: AgentStatus = "notInitialized";
|
||||
private api: TabbyApi;
|
||||
|
|
@ -53,12 +55,14 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
|||
static async create(options?: Partial<TabbyAgentOptions>): Promise<TabbyAgent> {
|
||||
const agent = new TabbyAgent();
|
||||
agent.dataStore = options?.dataStore;
|
||||
agent.anonymousUsageLogger = await AnonymousUsageLogger.create({ dataStore: options?.dataStore });
|
||||
await agent.applyConfig();
|
||||
return agent;
|
||||
}
|
||||
|
||||
private async applyConfig() {
|
||||
allLoggers.forEach((logger) => (logger.level = this.config.logs.level));
|
||||
this.anonymousUsageLogger.disabled = this.config.anonymousUsageTracking.disable;
|
||||
if (this.config.server.endpoint !== this.auth?.endpoint) {
|
||||
this.auth = await Auth.create({ endpoint: this.config.server.endpoint, dataStore: this.dataStore });
|
||||
this.auth.on("updated", this.onAuthUpdated.bind(this));
|
||||
|
|
@ -140,6 +144,9 @@ export class TabbyAgent extends EventEmitter implements Agent {
|
|||
if (options.config) {
|
||||
await this.updateConfig(options.config);
|
||||
}
|
||||
await this.anonymousUsageLogger.event("AgentInitialized", {
|
||||
client: options.client,
|
||||
});
|
||||
this.logger.debug({ options }, "Initialized");
|
||||
return this.status !== "notInitialized";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,4 +30,15 @@ export class ApiService {
|
|||
query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param body object for anonymous usage tracking
|
||||
*/
|
||||
public usage(body: any): CancelablePromise<any> {
|
||||
return this.httpRequest.request({
|
||||
method: "POST",
|
||||
url: "/usage",
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { isBrowser } from "./utils";
|
||||
|
||||
export type StoredData = {
|
||||
anonymousId: string;
|
||||
auth: { [endpoint: string]: { jwt: string } };
|
||||
};
|
||||
|
||||
|
|
@ -8,8 +11,7 @@ export interface DataStore {
|
|||
save(): PromiseLike<void>;
|
||||
}
|
||||
|
||||
declare var IS_BROWSER: boolean;
|
||||
export const dataStore: DataStore = IS_BROWSER
|
||||
export const dataStore: DataStore = isBrowser
|
||||
? null
|
||||
: (() => {
|
||||
const dataFile = require("path").join(require("os").homedir(), ".tabby", "agent", "data.json");
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import pino from "pino";
|
||||
|
||||
declare var IS_BROWSER: boolean;
|
||||
import { isBrowser } from "./utils";
|
||||
|
||||
/**
|
||||
* Stream not available in browser, will use default console output.
|
||||
*/
|
||||
const stream = IS_BROWSER
|
||||
const stream = isBrowser
|
||||
? null
|
||||
: /**
|
||||
* Default rotating file locate at `~/.tabby/agent-logs/`.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
declare const IS_BROWSER: boolean;
|
||||
export const isBrowser = IS_BROWSER;
|
||||
|
||||
export function splitLines(input: string) {
|
||||
return input.match(/.*(?:$|\r?\n)/g).filter(Boolean) // Split lines and keep newline character
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
"target": "ES2020",
|
||||
"lib": ["ES2020", "dom"],
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["./src"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,11 @@
|
|||
"patternErrorMessage": "Please enter a validate http or https URL.",
|
||||
"markdownDescription": "Specifies the url of [Tabby Server](https://github.com/TabbyML/tabby)."
|
||||
},
|
||||
"tabby.disableAnonymousUsageTracking": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Disable anonymous usage tracking."
|
||||
},
|
||||
"tabby.suggestionDelay": {
|
||||
"type": "number",
|
||||
"default": 150,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ function getWorkspaceConfiguration(): Partial<AgentConfig> {
|
|||
level: agentLogs,
|
||||
};
|
||||
}
|
||||
const disableAnonymousUsageTracking = configuration.get<boolean>("disableAnonymousUsageTracking", false);
|
||||
config.anonymousUsageTracking = {
|
||||
disable: disableAnonymousUsageTracking,
|
||||
};
|
||||
return config;
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +48,7 @@ export async function createAgentInstance(context: ExtensionContext): Promise<Ta
|
|||
const agent = await TabbyAgent.create({ dataStore: env.appHost === "desktop" ? undefined : extensionDataStore });
|
||||
agent.initialize({
|
||||
config: getWorkspaceConfiguration(),
|
||||
client: `${env.appName} ${env.appHost} ${version}`,
|
||||
client: `${env.appName} ${env.appHost} ${version}, ${context.extension.id} ${context.extension.packageJSON.version}`,
|
||||
});
|
||||
workspace.onDidChangeConfiguration((event) => {
|
||||
if (event.affectsConfiguration("tabby")) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue