From 0c25a9881515e1f25e58e0d7309ca18a96a2f01c Mon Sep 17 00:00:00 2001 From: Nika Salamadze Date: Tue, 1 Aug 2023 10:36:45 -0400 Subject: [PATCH] chore: add RudderStack for event tracking --- .github/workflows/runTests.yml | 8 +++++- .gitignore | 1 + .vscode/tasks.json | 20 +++++++++---- DEVELOPMENT.md | 12 ++++++++ package.json | 7 +++-- src/RudderStackService.ts | 43 +++++++++++++++++++++++++++ src/StateManager.ts | 8 +++++- src/cli/baseCLIController.ts | 8 +++--- src/components/SidebarProvider.ts | 2 +- src/extension.ts | 48 +++++++++++++++++++++++++------ 10 files changed, 134 insertions(+), 23 deletions(-) create mode 100644 src/RudderStackService.ts diff --git a/.github/workflows/runTests.yml b/.github/workflows/runTests.yml index 268e3e0..a514f31 100644 --- a/.github/workflows/runTests.yml +++ b/.github/workflows/runTests.yml @@ -12,18 +12,24 @@ on: jobs: test: name: Test - runs-on: windows-latest + runs-on: ubuntu-latest env: DISPLAY: ':99.0' steps: + - name: Start xvfb + run: /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & echo ">>> Started xvfb" + - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '16.x' cache: 'yarn' + - name: Install dependencies run: yarn --immutable && yarn install + - name: Lint Project run: yarn lint + - name: Test Project run: yarn test diff --git a/.gitignore b/.gitignore index 0efe886..f1037da 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules .env .idea .DS_STORE +/src/analytics.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 89cdd10..49a60a5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,6 +4,7 @@ "version": "2.0.0", "tasks": [ { + "label": "watch", "type": "npm", "script": "watch", "problemMatcher": "$tsc-watch", @@ -11,15 +12,24 @@ "presentation": { "reveal": "never" }, - "group": { - "kind": "build", - "isDefault": true - } }, { "type": "npm", "script": "compile", - "problemMatcher": [] + "problemMatcher": [], + }, + { + "label": "create-analytics-file", + "type": "npm", + "script": "create-analytics-file", + }, + { + "label": "dev", + "dependsOn": ["create-analytics-file", "watch"], + "group": { + "kind": "build", + "isDefault": true + } } ] } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index f953c70..d5327c8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -47,3 +47,15 @@ - Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). - [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. - Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). + +## [Internal] RudderStack setup + +- In order to track events during development, you will need to get an authorization token from RudderStack. + +1. Go to [RudderStack](https://app.rudderstack.com/) and login with the credentials stored in 1Password +2. Go to `Sources` and select the `VS Code Extension` source +3. On the `Setup` tab, copy the `Write Key` +4. Go to a [Basic Authentication Header Generator +](https://www.blitter.se/utils/basic-authentication-header-generator/) to generate a token. +5. Use the `Write Key` as the username and leave the password blank +6. Copy the generated token and paste it in `src/analytics.ts`(if this file does not exist, run the extension and it should be automatically generated for you) as the value of the `RUDDERSTACK_KEY` variable diff --git a/package.json b/package.json index 844170f..6cb7984 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ }, "devcycle-feature-flags.sendMetrics": { "type": "boolean", - "default": true, + "default": false, "description": "Allow DevCycle to send usage metrics." } } @@ -125,11 +125,12 @@ "vscode:prepublish": "yarn run compile", "compile": "tsc -p ./", "watch": "tsc -watch -p ./", - "pretest": "yarn run compile && yarn run lint", + "pretest": "yarn create-analytics-file && yarn run compile && yarn run lint", "lint": "eslint src --ext ts", "test": "node ./out/test/runTest.js", "publish": "vsce publish", - "package": "vsce package" + "package": "vsce package", + "create-analytics-file": "test -f ./src/analytics.ts || echo 'export const RUDDERSTACK_KEY = \"\";' > ./src/analytics.ts" }, "devDependencies": { "@types/chai": "^4.3.5", diff --git a/src/RudderStackService.ts b/src/RudderStackService.ts new file mode 100644 index 0000000..7f525dc --- /dev/null +++ b/src/RudderStackService.ts @@ -0,0 +1,43 @@ +import axios from "axios"; +import * as vscode from 'vscode' +import { RUDDERSTACK_KEY } from "./analytics"; +import { getOrganizationId } from "./cli"; + +type RudderstackEvent = { + event: string, + userId: string, + properties: Record +} + +const rudderstackClient = axios.create({ + baseURL: 'https://taplyticsncs.dataplane.rudderstack.com/v1/', + headers: { + Authorization: `Basic ${RUDDERSTACK_KEY}`, + 'Content-Type': 'application/json' + } +}) + +export const trackRudderstackEvent = async ( + eventName: string +): Promise => { + const sendMetrics = vscode.workspace.getConfiguration('devcycle-feature-flags').get('sendMetrics') + if (sendMetrics) { + const orgId = getOrganizationId() + if (!orgId) { return } + const event = { + event: eventName, + userId: orgId, + properties: { + a0_organization: orgId + } + } + await rudderstackClient.post('track', event).catch((error) => { + if (!axios.isAxiosError(error)) { return } + if (error?.response?.status === 401) { + console.error('Failed to send event. Analytics key is invalid.') + } else { + console.error('Failed to send event. Status: ', error?.response?.status) + } + }) + } +} diff --git a/src/StateManager.ts b/src/StateManager.ts index 11bc3b5..dcd0806 100644 --- a/src/StateManager.ts +++ b/src/StateManager.ts @@ -10,6 +10,7 @@ export const enum KEYS { ORGANIZATION = 'organization', SEND_METRICS_PROMPTED = 'send_metrics_prompted', CODE_USAGE_KEYS = 'code_usage_keys', + EXTENSION_INSTALLED = 'extension_installed', } export class StateManager { @@ -18,7 +19,12 @@ export class StateManager { static clearState() { this.workspaceState.keys().forEach((key) => { - if (key !== KEYS.ORGANIZATION && key !== KEYS.PROJECT_ID && key !== KEYS.PROJECT_NAME) { + if ( + key !== KEYS.PROJECT_ID && + key !== KEYS.PROJECT_NAME && + key !== KEYS.ORGANIZATION && + key !== KEYS.EXTENSION_INSTALLED + ) { this.workspaceState.update(key, undefined) } }) diff --git a/src/cli/baseCLIController.ts b/src/cli/baseCLIController.ts index eeee08e..0219d97 100644 --- a/src/cli/baseCLIController.ts +++ b/src/cli/baseCLIController.ts @@ -81,7 +81,7 @@ export async function login() { await chooseOrganization(organizations) const org = StateManager.getState(KEYS.ORGANIZATION) const project = StateManager.getState(KEYS.PROJECT_ID) - if (!org || !project) return + if (!org || !project) { return } await vscode.commands.executeCommand( 'setContext', @@ -108,7 +108,7 @@ export async function chooseOrganization(organizations: Organization[]) { ignoreFocusOut: true, title: 'Select DevCycle Organization', }))?.value - if (!organization) return + if (!organization) { return } StateManager.setState(KEYS.ORGANIZATION, organization) StateManager.setState(KEYS.PROJECT_ID, undefined) @@ -134,7 +134,7 @@ export async function chooseProject(projects: string[]) { ignoreFocusOut: true, title: 'Select DevCycle Project', }) - if (!project) return + if (!project) { return } const { code, error } = await execDvc(`projects select --project=${project}`) if (code === 0) { await vscode.commands.executeCommand( @@ -192,7 +192,7 @@ export async function execDvc(cmd: string) { vscode.workspace.getConfiguration('devcycle-feature-flags').get('cli') || 'dvc' const project_id = StateManager.getState(KEYS.PROJECT_ID) - let shellCommand = `${cli} ${cmd} --headless` + let shellCommand = `${cli} ${cmd} --headless --caller vs_code_extension` if (project_id) shellCommand += ` --project ${project_id}` return execShell(shellCommand) } diff --git a/src/components/SidebarProvider.ts b/src/components/SidebarProvider.ts index cf4cb7a..b0607d2 100644 --- a/src/components/SidebarProvider.ts +++ b/src/components/SidebarProvider.ts @@ -41,7 +41,7 @@ export class SidebarProvider implements vscode.WebviewViewProvider { const org = StateManager.getState(KEYS.ORGANIZATION) const project = StateManager.getState(KEYS.PROJECT_ID) - if (!org || !project) return + if (!org || !project) { return } await vscode.commands.executeCommand( 'setContext', diff --git a/src/extension.ts b/src/extension.ts index e3163d9..973f055 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,6 +7,8 @@ import { SidebarProvider } from './components/SidebarProvider' import { UsagesTreeProvider } from './components/UsagesTree' import { getHoverString } from './components/hoverCard' +import { trackRudderstackEvent } from './RudderStackService' +import { CodeUsageNode } from './components/UsagesTree/CodeUsageNode' Object.defineProperty(exports, '__esModule', { value: true }) exports.deactivate = exports.activate = void 0 @@ -29,12 +31,23 @@ export const activate = async (context: vscode.ExtensionContext) => { StateManager.setState(KEYS.SEND_METRICS_PROMPTED, true) }) } + + if (!StateManager.globalState.get(KEYS.EXTENSION_INSTALLED)) { + await StateManager.globalState.update(KEYS.EXTENSION_INSTALLED, true) + trackRudderstackEvent('Extension Installed') + } const autoLogin = vscode.workspace .getConfiguration('devcycle-feature-flags') .get('loginOnWorkspaceOpen') const sidebarProvider = new SidebarProvider(context.extensionUri) + context.subscriptions.push( + vscode.window.registerWebviewViewProvider( + 'devcycle-sidebar', + sidebarProvider + ), + ) const rootPath = vscode.workspace.workspaceFolders && @@ -42,19 +55,37 @@ export const activate = async (context: vscode.ExtensionContext) => { ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined const usagesDataProvider = new UsagesTreeProvider(rootPath, context) - context.subscriptions.push( - vscode.window.registerWebviewViewProvider( - 'devcycle-sidebar', - sidebarProvider, - ), - ) - vscode.window.registerTreeDataProvider( + const usagesTreeView = vscode.window.createTreeView( 'devcycleCodeUsages', - usagesDataProvider, + { treeDataProvider: usagesDataProvider }, ) + usagesTreeView.onDidChangeVisibility(async (e) => { + trackRudderstackEvent('Usages Viewed') + }) + + context.subscriptions.push( + vscode.commands.registerCommand( + 'devcycle-featureflags.usagesNodeClicked', + async (node: CodeUsageNode) => { + trackRudderstackEvent('Code Usage Clicked') + } + ) + ) + + usagesTreeView.onDidChangeSelection((e) => { + const node = e.selection[0] + if (node instanceof CodeUsageNode && node.type === 'usage') { + vscode.commands.executeCommand( + 'devcycle-featureflags.usagesNodeClicked', + node + ) + } + }) + context.subscriptions.push( vscode.commands.registerCommand('devcycle-feature-flags.init', async () => { + trackRudderstackEvent('Init Command Ran') await init() }), ) @@ -72,6 +103,7 @@ export const activate = async (context: vscode.ExtensionContext) => { vscode.commands.registerCommand( 'devcycle-feature-flags.logout', async () => { + trackRudderstackEvent('Logout Command Ran') await Promise.all([ StateManager.clearState(), vscode.commands.executeCommand(