Skip to content

Commit

Permalink
chore: add RudderStack for event tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
nsalamad committed Aug 1, 2023
1 parent 2f01f21 commit b35d2cb
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 25 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ node_modules
.env
.idea
.DS_STORE
/src/analytics.ts
20 changes: 15 additions & 5 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,32 @@
"version": "2.0.0",
"tasks": [
{
"label": "watch",
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"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
}
}
]
}
12 changes: 12 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
},
"devcycle-featureflags.sendMetrics": {
"type": "boolean",
"default": true,
"default": false,
"description": "Allow DevCycle to send usage metrics."
}
}
Expand All @@ -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",
Expand Down
33 changes: 33 additions & 0 deletions src/RudderStackService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import axios from "axios";
import * as vscode from 'vscode'
import { RUDDERSTACK_KEY } from "./analytics";

type RudderstackEvent = {
event: string,
userId: string,
properties: Record<string, unknown>
}

const rudderstackClient = axios.create({
baseURL: 'https://taplyticsncs.dataplane.rudderstack.com/v1/',
headers: {
Authorization: `Basic ${RUDDERSTACK_KEY}`,
'Content-Type': 'application/json'
}
})

export const trackRudderstackEvent = async (
event: RudderstackEvent
): Promise<void> => {
const sendMetrics = vscode.workspace.getConfiguration('devcycle-featureflags').get('sendMetrics')
if (sendMetrics) {
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)
}
})
}
}
7 changes: 6 additions & 1 deletion src/StateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const enum KEYS {
VARIABLES = 'variables',
FEATURE_CONFIGURATIONS = 'feature_configurations',
ENVIRONMENTS = 'environments',
EXTENSION_INSTALLED = 'extension_installed',
ORGANIZATION_ID = 'organization_id',
}

Expand All @@ -16,7 +17,11 @@ export class StateManager {

static clearState() {
this.workspaceState.keys().forEach((key) => {
if (key !== KEYS.PROJECT_ID && key !== KEYS.PROJECT_NAME) {
if (
key !== KEYS.PROJECT_ID &&
key !== KEYS.PROJECT_NAME &&
key !== KEYS.EXTENSION_INSTALLED
) {
this.workspaceState.update(key, undefined)
}
})
Expand Down
2 changes: 2 additions & 0 deletions src/api/getToken.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import axios from 'axios'
import { CLIENT_KEYS, SecretStateManager } from '../SecretStateManager'
import { KEYS, StateManager } from '../StateManager'

export const getToken = async (id: string, secret: string) => {
return await axios({
Expand Down
2 changes: 1 addition & 1 deletion src/cli/baseCLIController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export async function execDvc(cmd: string) {
vscode.workspace.getConfiguration('devcycle-featureflags').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)
}
Expand Down
8 changes: 4 additions & 4 deletions src/cli/cliUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ export const getCombinedVariableDetails = async (
}
}

export const getOrganizationId = async () => {
const cachedOrganizationId = StateManager.getState(KEYS.ORGANIZATION_ID)
export const getOrganizationId = async (): Promise<string | undefined> => {
const cachedOrganizationId = StateManager.getState(KEYS.ORGANIZATION_ID) as string | undefined
if (cachedOrganizationId) {
return cachedOrganizationId
}
Expand All @@ -84,7 +84,7 @@ export const getOrganizationId = async () => {
}
const token = await getToken(client_id, client_secret)
const jsonToken = JSON.parse(Buffer.from(token.access_token.split('.')[1], 'base64').toString())
const orgId = jsonToken['https://devcycle.com/org_id']
const orgId = jsonToken['https://devcycle.com/org_id'] as string
StateManager.setState(KEYS.ORGANIZATION_ID, orgId)
return orgId
}
}
99 changes: 88 additions & 11 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
;('use strict')
import * as vscode from 'vscode'
import { StateManager } from './StateManager'
import { init, logout, status as cliStatus } from './cli'
import { KEYS, StateManager } from './StateManager'
import { init, logout, status as cliStatus, getOrganizationId } from './cli'
import { SecretStateManager } from './SecretStateManager'
import { autoLoginIfHaveCredentials } from './utils/credentials'
import { SidebarProvider } from './components/SidebarProvider'

import { UsagesTreeProvider } from './components/UsagesTreeProvider'
import { UsagesTreeProvider, CodeUsageNode } from './components/UsagesTreeProvider'
import { getHoverString } from './components/hoverCard'
import { trackRudderstackEvent } from './RudderStackService'

Object.defineProperty(exports, '__esModule', { value: true })
exports.deactivate = exports.activate = void 0
Expand All @@ -21,31 +22,97 @@ export const activate = async (context: vscode.ExtensionContext) => {
SecretStateManager.init(context)
StateManager.globalState = context.globalState
StateManager.workspaceState = context.workspaceState

if (!StateManager.globalState.get(KEYS.EXTENSION_INSTALLED)) {
await StateManager.globalState.update(KEYS.EXTENSION_INSTALLED, true)
const orgId = await getOrganizationId()
if (orgId) {
trackRudderstackEvent({
event: 'Extension Installed',
userId: orgId,
properties: {
a0_organization: orgId
}
})
}
}

const autoLogin = vscode.workspace
.getConfiguration('devcycle-featureflags')
.get('loginOnWorkspaceOpen')

const sidebarProvider = new SidebarProvider(context.extensionUri)
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(
'devcycle-sidebar',
sidebarProvider
),
)

const rootPath =
vscode.workspace.workspaceFolders &&
vscode.workspace.workspaceFolders.length > 0
? 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) => {
const orgId = await getOrganizationId()
if (orgId && e.visible) {
trackRudderstackEvent({
event: 'Usages Viewed',
userId: orgId,
properties: {
a0_organization: orgId
}
})
}
})

context.subscriptions.push(
vscode.commands.registerCommand(
'devcycle-featureflags.usagesNodeClicked',
async (node: CodeUsageNode) => {
const orgId = await getOrganizationId()
if (orgId) {
trackRudderstackEvent({
event: 'Code Usage Clicked',
userId: orgId,
properties: {
a0_organization: orgId
}
})
}
}
)
)

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-featureflags.init', async () => {
const orgId = await getOrganizationId()
if (orgId) {
trackRudderstackEvent({
event: 'Init Command Ran',
userId: orgId,
properties: {
a0_organization: orgId
}
})
}
await init()
}),
)
Expand All @@ -63,6 +130,16 @@ export const activate = async (context: vscode.ExtensionContext) => {
vscode.commands.registerCommand(
'devcycle-featureflags.logout',
async () => {
const orgId = await getOrganizationId()
if (orgId) {
trackRudderstackEvent({
event: 'Logout Command Ran',
userId: orgId,
properties: {
a0_organization: orgId
}
})
}
await Promise.all([
SecretStateManager.instance.clearSecrets(),
StateManager.clearState(),
Expand Down

0 comments on commit b35d2cb

Please sign in to comment.