diff --git a/vsce/package.json b/vsce/package.json index d477ffe..2e2277a 100644 --- a/vsce/package.json +++ b/vsce/package.json @@ -241,6 +241,7 @@ "pako": "^2.1.0", "rimraf": "^5.0.1", "uuid": "^9.0.1", - "ws": "^8.13.0" + "ws": "^8.13.0", + "mixpanel": "^0.18.0" } } diff --git a/vsce/src/chat/commands.ts b/vsce/src/chat/commands.ts index eeaa9aa..9246dba 100644 --- a/vsce/src/chat/commands.ts +++ b/vsce/src/chat/commands.ts @@ -129,6 +129,10 @@ export async function initChat( chat.setMessages(message.msgs); // TODO: Move to addMessage to reduce communication overhead storeChat(chat); break; + case "getUserId": + const userId = vscode.workspace.getConfiguration().get('NeatworkAi.neatcoder.userId'); + panel.webview.postMessage({ command: 'userId', userId: userId }); + break; } }, undefined, diff --git a/vsce/src/config.ts b/vsce/src/config.ts new file mode 100644 index 0000000..38a5802 --- /dev/null +++ b/vsce/src/config.ts @@ -0,0 +1,3 @@ +export const CONFIG = { + mixpanelToken: 'fad3409b4b79d03a837adc7db17f0e97', +}; diff --git a/vsce/src/core/workflows/initCodeBase.ts b/vsce/src/core/workflows/initCodeBase.ts index 4751e13..ffdd345 100644 --- a/vsce/src/core/workflows/initCodeBase.ts +++ b/vsce/src/core/workflows/initCodeBase.ts @@ -3,6 +3,7 @@ import * as wasm from "../../../pkg/neatcoder"; import { appDataManager } from "../appData"; import { addLanguage } from "../commands/addLanguage"; import { startLoading, stopLoading } from "../../utils/statusBar"; +import MixpanelHelper from "../../utils/mixpanelHelper"; /** * Asynchronously initiates a prompt to gather user input and starts processing based on the input. @@ -34,6 +35,9 @@ export async function initCodeBase( }); if (userInput !== undefined) { + let mixpanel = MixpanelHelper.getInstance(); + mixpanel.trackEvent('initCodeBase', { userInput: userInput }); + startLoading("Prompting the LLM.."); await appManager.initCodeBase(llmParams, userInput); stopLoading(); diff --git a/vsce/src/extension.ts b/vsce/src/extension.ts index daef3a7..2acbbf7 100644 --- a/vsce/src/extension.ts +++ b/vsce/src/extension.ts @@ -25,6 +25,7 @@ import { initCodeBase, appDataManager, setupDotNeatWatcher } from "./core"; import { getOrSetApiKey, initStatusBar, initLogger, logger } from "./utils"; import { ChatProvider, initChat, setupChatWatcher } from "./chat"; import { getOrSetModelVersion, setModelVersion } from "./utils/utils"; +import MixpanelHelper from "./utils/mixpanelHelper"; // Declare activePanels at the top-level to make it accessible throughout your extension's main script. let configWatcher: fs.FSWatcher | undefined; // TODO: remove, not being used. @@ -54,6 +55,10 @@ export async function activate(context: vscode.ExtensionContext) { const interfacesProvider = new InterfacesProvider(); const chatProvider = new ChatProvider(); + // init Mixpanel + let mixpanel = MixpanelHelper.getInstance(); + mixpanel.trackEvent("activate"); + // Read or Initialize Application state let appManager = new appDataManager(jobQueueProvider, auditTrailProvider); @@ -102,6 +107,7 @@ export async function activate(context: vscode.ExtensionContext) { // Register the Chat command vscode.commands.registerCommand("extension.createChat", () => { + mixpanel.trackEvent('createChat'); initChat(context); }); @@ -114,24 +120,30 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand("extension.addDatastore", async () => { + mixpanel.trackEvent('addDatastore'); addInterface(wasm.InterfaceType.Database, interfacesProvider); }) ); context.subscriptions.push( vscode.commands.registerCommand("extension.addApi", async () => { + mixpanel.trackEvent('addApi'); addInterface(wasm.InterfaceType.Api, interfacesProvider); }) ); context.subscriptions.push( - vscode.commands.registerCommand("extension.addSchema", addSchema) + vscode.commands.registerCommand("extension.addSchema", async (item: InterfaceItem) => { + mixpanel.trackEvent('addSchema'); + await addSchema(item); + }) ); context.subscriptions.push( vscode.commands.registerCommand( "extension.removeInterface", (item: InterfaceItem) => { + mixpanel.trackEvent('removeInterface'); removeInterface(item); } ) @@ -141,6 +153,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand( "extension.removeSchema", (item: InterfaceItem) => { + mixpanel.trackEvent('removeSchema'); removeSchema(item); } ) @@ -150,6 +163,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand( "extension.runTask", async (taskView: TaskView) => { + mixpanel.trackEvent('runTask'); let llmParams = await getLLMParams(); await runTask(taskView, llmParams, appManager); } @@ -160,6 +174,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand( "extension.removeTask", (taskView: TaskView) => { + mixpanel.trackEvent('removeTask'); removeTask(taskView, appManager); } ) @@ -167,18 +182,21 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.commands.registerCommand("extension.removeAllTasks", () => { + mixpanel.trackEvent('removeAllTasks'); removeAllTasks(appManager); }) ); context.subscriptions.push( vscode.commands.registerCommand("extension.chooseModel", async () => { + mixpanel.trackEvent('chooseModel'); await setModelVersion(); }) ); context.subscriptions.push( vscode.commands.registerCommand("extension.runAllTasks", async () => { + mixpanel.trackEvent('runAllTasks'); let llmParams = await getLLMParams(); runAllTasks(llmParams, appManager); }) @@ -188,6 +206,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand( "extension.retryTask", async (taskView: TaskView) => { + mixpanel.trackEvent('retryTask'); let llmParams = await getLLMParams(); await retryTask(taskView, llmParams, appManager); } @@ -208,6 +227,9 @@ async function getLLMParams(): Promise { // This method is called when the extension is deactivated export function deactivate() { + let mixpanel = MixpanelHelper.getInstance(); + mixpanel.trackEvent("deactivate"); + if (configWatcher) { configWatcher.close(); } diff --git a/vsce/src/utils/mixpanelHelper.ts b/vsce/src/utils/mixpanelHelper.ts new file mode 100644 index 0000000..92875af --- /dev/null +++ b/vsce/src/utils/mixpanelHelper.ts @@ -0,0 +1,63 @@ +import * as mixpanel from 'mixpanel'; +import * as vscode from 'vscode'; +import { v4 as uuidv4 } from 'uuid'; +import { CONFIG } from '../config'; +import * as os from 'os'; + +class MixpanelHelper { + private static instance: MixpanelHelper; + private mixpanelInstance: mixpanel.Mixpanel; + private userId: string; + + private constructor(token: string, config?: mixpanel.InitConfig) { + const defaultConfig = { + geolocate: true + }; + + // If there's any user-provided config, it will overwrite the defaults + const finalConfig = { ...defaultConfig, ...config }; + + this.mixpanelInstance = mixpanel.init(token, finalConfig); + + // Retrieve or generate the userId + this.userId = this.getUserId(); + } + + static getInstance(config?: mixpanel.InitConfig): MixpanelHelper { + if (!MixpanelHelper.instance) { + MixpanelHelper.instance = new MixpanelHelper(CONFIG.mixpanelToken, config); + } + return MixpanelHelper.instance; + } + + private getUserId(): string { + const userId = vscode.workspace.getConfiguration().get('NeatworkAi.neatcoder.userId'); + if (userId) { + return userId; + } + + const newUserId = uuidv4(); + vscode.workspace.getConfiguration().update('NeatworkAi.neatcoder.userId', newUserId, vscode.ConfigurationTarget.Global); + return newUserId; + } + + private getExtensionVersion(): string { + const extension = vscode.extensions.getExtension('NeatworkAi.neatcoder'); + return extension?.packageJSON.version || 'unknown'; + } + + trackEvent(eventName: string, properties: mixpanel.PropertyDict = {}): void { + properties.distinct_id = this.userId; + properties.$os = os.type(); + properties.extensionVersion = this.getExtensionVersion(); + properties.moduleName = 'VSCodeExtension'; + + this.mixpanelInstance.track(eventName, properties, (err) => { + if (err) { + console.error('Error sending event to Mixpanel:', err); + } + }); + } +} + +export default MixpanelHelper; diff --git a/webview/package.json b/webview/package.json index 9dc2f31..b804c35 100644 --- a/webview/package.json +++ b/webview/package.json @@ -14,6 +14,7 @@ "font-awesome": "^4.7.0", "highlight.js": "^11.9.0", "marked": "^9.1.2", + "mixpanel-browser": "^2.47.0", "quill": "^1.3.7", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -51,6 +52,7 @@ "devDependencies": { "@types/marked": "^6.0.0", "@types/quill": "^2.0.13", - "@types/turndown": "^5.0.3" + "@types/turndown": "^5.0.3", + "@types/mixpanel-browser": "^2.47.4" } } diff --git a/webview/src/App.tsx b/webview/src/App.tsx index d9a05fb..826ab89 100644 --- a/webview/src/App.tsx +++ b/webview/src/App.tsx @@ -1,8 +1,19 @@ // import React from 'react'; import './App.css'; import ChatContainer from './ components/chatContainer'; +import { useEffect } from 'react'; +import MixpanelWebviewHelper from './mixpanel-webview-helper'; +// import 'highlight.js/styles/atom-one-dark.css'; function App() { + useEffect(() => { + try { + const mixpanelWebview = MixpanelWebviewHelper.getInstance(); + console.log("MixpanelWebviewHelper initialized", mixpanelWebview); + } catch (error) { + console.error("Error initializing MixpanelWebviewHelper:", error); + } + }, []); return (
diff --git a/webview/src/config.ts b/webview/src/config.ts new file mode 100644 index 0000000..38a5802 --- /dev/null +++ b/webview/src/config.ts @@ -0,0 +1,3 @@ +export const CONFIG = { + mixpanelToken: 'fad3409b4b79d03a837adc7db17f0e97', +}; diff --git a/webview/src/index.tsx b/webview/src/index.tsx index 032464f..e4ead81 100644 --- a/webview/src/index.tsx +++ b/webview/src/index.tsx @@ -3,6 +3,8 @@ import ReactDOM from 'react-dom/client'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; +import MixpanelWebviewHelper from './mixpanel-webview-helper'; +import { Metric } from 'web-vitals'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement @@ -13,7 +15,17 @@ root.render( ); -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(console.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); +function sendToMixpanel(metric: Metric) { + const mixpanelWebview = MixpanelWebviewHelper.getInstance(); + + const eventData = { + id: metric.id, + name: metric.name, + value: metric.value, + delta: metric.delta, + }; + + mixpanelWebview.trackEvent('WebVitals', eventData); +} + +reportWebVitals(sendToMixpanel); diff --git a/webview/src/mixpanel-webview-helper.ts b/webview/src/mixpanel-webview-helper.ts new file mode 100644 index 0000000..87f8d72 --- /dev/null +++ b/webview/src/mixpanel-webview-helper.ts @@ -0,0 +1,45 @@ +import mixpanel, { Dict } from 'mixpanel-browser'; +import { CONFIG } from './config'; + +class MixpanelWebviewHelper { + private static instance: MixpanelWebviewHelper; + private userId: string | undefined; + + private constructor() { + mixpanel.init(CONFIG.mixpanelToken); + this.initializeUserId(); + } + + private async initializeUserId() { + this.userId = await this.getUserIdFromExtension(); + mixpanel.identify(this.userId); + } + + static getInstance(): MixpanelWebviewHelper { + if (!MixpanelWebviewHelper.instance) { + MixpanelWebviewHelper.instance = new MixpanelWebviewHelper(); + } + return MixpanelWebviewHelper.instance; + } + + private getUserIdFromExtension(): Promise { + return new Promise((resolve) => { + const vscode = acquireVsCodeApi(); + vscode.postMessage({ command: 'getUserId' }); + + window.addEventListener('message', event => { + const message = event.data; + if (message.command === 'userId') { + resolve(message.userId); + } + }); + }); + } + + trackEvent(eventName: string, properties: Dict = {}): void { + properties.moduleName = 'Webview'; + mixpanel.track(eventName, properties); + } +} + +export default MixpanelWebviewHelper;