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
Zhiming Ma 2023-06-16 16:58:50 +08:00 committed by GitHub
parent 8ee700089f
commit 534d3d5ea7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 3329 additions and 2515 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -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

View File

@ -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");
});
}
}

View File

@ -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";
}

View File

@ -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,
});
}
}

View File

@ -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");

View File

@ -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/`.

View File

@ -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
}

View File

@ -4,7 +4,8 @@
"target": "ES2020",
"lib": ["ES2020", "dom"],
"sourceMap": true,
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"include": ["./src"]
}

View File

@ -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,

View File

@ -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")) {