Skip to content

Commit

Permalink
Merge pull request #53 from pullflow/epic/telemetry
Browse files Browse the repository at this point in the history
Epic: Telemetry for extension
  • Loading branch information
srzainab authored Jan 24, 2024
2 parents 3895f0a + bc06a40 commit c773eae
Show file tree
Hide file tree
Showing 17 changed files with 323 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"files.trimTrailingWhitespace": true,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
Expand Down
21 changes: 21 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,22 @@
"fontCharacter": "\\e90c"
}
}
},
"configuration": {
"id": "pullflow",
"title": "Pullflow",
"properties": {
"pullflow.telemetry.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Enable Pullflow to transmit product usage telemetry. \n\n_**Important:** To activate telemetry transmission, both this setting and the VS Code telemetry option must be enabled. Telemetry will not be sent if either of these settings is disabled._"
},
"pullflow.automaticFlowDetection.enabled": {
"type": "boolean",
"default": true,
"markdownDescription": "Alow Pullflow to automatically detect flow state based on keystrokes.\n\n_**Note:** Extension must be reloaded for this to take affect._"
}
}
}
},
"scripts": {
Expand Down Expand Up @@ -204,6 +220,11 @@
},
"dependencies": {
"@octokit/rest": "^19.0.3",
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.46.0",
"@opentelemetry/resources": "^1.19.0",
"@opentelemetry/sdk-trace-base": "^1.19.0",
"@opentelemetry/semantic-conventions": "^1.19.0",
"dotenv": "^16.0.3",
"jest": "^29.5.0",
"moment": "^2.29.4",
Expand Down
1 change: 1 addition & 0 deletions src/commands/activePullRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const ActivePullRequests = (
const activePullRequestItems = ActivePullRequestItems.get(codeReviews)

QuickPick.create({
context,
items: activePullRequestItems,
title: 'My Active Pull Requests',
placeholder: 'select pull request',
Expand Down
8 changes: 4 additions & 4 deletions src/commands/signOut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ export const SignOut = async ({
pollIntervalId: ReturnType<typeof setInterval>
focusStateEvent: ReturnType<typeof window.onDidChangeWindowState>
presenceInterval: {
userFlowIntervalId: ReturnType<typeof setInterval>
textEditorEvent: ReturnType<typeof window.onDidChangeTextEditorSelection>
clearFlowInterval: Function
disposeTextEditorEvent: Function
}
}) => {
await context.secrets.store(AppConfig.app.sessionSecret, '')
await Store.clear(context)
StatusBar.update({ context, statusBar, state: StatusBarState.SignedOut })
clearInterval(pollIntervalId) // stopping polling interval
clearInterval(presenceInterval.userFlowIntervalId) // stopping user flow interval
focusStateEvent.dispose() // removing focus event listener
presenceInterval.textEditorEvent.dispose() // removing text editor event listener
presenceInterval.clearFlowInterval() // stopping user flow interval
presenceInterval.disposeTextEditorEvent() // removing text editor event listener
}
4 changes: 4 additions & 0 deletions src/pullRequestQuickActions/pullRequestQuickActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ const subject = () => {
jest.mock('../models/presence', () => mockPresence)
jest.mock('../utils/pullRequestsState', () => mockPullRequestState)
jest.mock('../views/quickpicks/spaceUserPicker', () => mockSpaceUserPicker)
jest.mock('../views/quickpicks/timePicker', () => mockTimerPicker)
return require('./pullRequestQuickActions').PullRequestQuickActions
}

Expand Down Expand Up @@ -120,3 +121,6 @@ const mockModel = {
const mockSession = {
accessToken: 'mock_access_token',
}
const mockTimerPicker = {
timePicker: jest.fn(),
}
2 changes: 1 addition & 1 deletion src/pullRequestQuickActions/pullRequestQuickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ export const PullRequestQuickActions = {
return true
}

timePicker({ title: 'Remind me in', onDidChangeSelection })
timePicker({ title: 'Remind me in', onDidChangeSelection, context })
},
}

Expand Down
14 changes: 13 additions & 1 deletion src/userPresence/trackUserPresence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ export const trackUserPresence = (
context: ExtensionContext,
statusBar: StatusBarItem
) => {
const flowEnabled = Store.get(context)?.isFlowDetectionEnabled
if (!flowEnabled) {
log.info(`user disabled flow detection`, module)
return {
clearFlowInterval: () => {},
disposeTextEditorEvent: () => {},
}
}

log.info(`started tracking user flow`, module)
const userFlowIntervalId = setInterval(async () => {
await UserPresence.update(context, statusBar)
Expand All @@ -18,7 +27,10 @@ export const trackUserPresence = (
incrementKeyStrokeCount(context)
})

return { userFlowIntervalId, textEditorEvent }
return {
clearFlowInterval: () => clearInterval(userFlowIntervalId),
disposeTextEditorEvent: () => textEditorEvent.dispose(),
}
}

const incrementKeyStrokeCount = (context: ExtensionContext) => {
Expand Down
1 change: 1 addition & 0 deletions src/utils/appConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export const AppConfig = {
pullflow: {
baseUrl: process.env['PULLFLOW_APP_URL'] as string,
graphqlUrl: `${process.env['PULLFLOW_APP_URL']}/api/graphql`,
telemetryUrl: process.env['PULLFLOW_TELEMETRY_URL'] as string,
},
}
45 changes: 45 additions & 0 deletions src/utils/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
WindowState,
commands,
window,
workspace,
} from 'vscode'
import { Store } from './store'
import { log } from './logger'
Expand All @@ -27,6 +28,9 @@ export const initialize = async ({
statusBar: StatusBarItem
}) => {
log.info('initializing extension', module)

await initializeConfiguration(context)

const errorCount = { count: 0 }
await PullRequestState.update({
context,
Expand Down Expand Up @@ -116,3 +120,44 @@ const setSpaceUsers = async ({
spaceUsers: spaceUsers.spaceUsers,
})
}

const extensionTelemetryFlag = () =>
getPullflowConfig('telemetry.enabled', true)

const vscodeTelemetryFlag = () =>
workspace.getConfiguration('telemetry').get<boolean>('enableTelemetry')

const initializeConfiguration = async (context: ExtensionContext) => {
await Store.set(context, {
isTelemetryEnabled: vscodeTelemetryFlag() && extensionTelemetryFlag(),
isFlowDetectionEnabled: !!getPullflowConfig(
'automaticFlowDetection.enabled'
),
})

const disposable = workspace.onDidChangeConfiguration(async (event) => {
if (
event.affectsConfiguration('telemetry.enableTelemetry') ||
event.affectsConfiguration('pullflow.telemetry.enabled')
) {
await Store.set(context, {
isTelemetryEnabled: vscodeTelemetryFlag() && extensionTelemetryFlag(),
})
}

if (event.affectsConfiguration('pullflow.automaticFlowDetection.enabled')) {
await Store.set(context, {
isFlowDetectionEnabled: !!getPullflowConfig(
'automaticFlowDetection.enabled'
),
})
}
})
context.subscriptions.push(disposable)
}

const getPullflowConfig = (key: string, defaultValue?: boolean) => {
const config = workspace.getConfiguration('pullflow')
const value = config.get<boolean>(key)
return value ?? defaultValue
}
99 changes: 99 additions & 0 deletions src/utils/trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/* eslint-disable no-unused-vars */
import { Span, Tracer, SpanKind, Attributes } from '@opentelemetry/api'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'
import { Resource } from '@opentelemetry/resources'
import {
BasicTracerProvider,
BatchSpanProcessor,
} from '@opentelemetry/sdk-trace-base'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import { ExtensionContext, extensions } from 'vscode'
import { TraceAttributes } from './types'
import { Store } from './store'
import { AppConfig } from './appConfig'
import { log } from './logger'

const extensionInfo = extensions.getExtension('Pullflow.pullflow')?.packageJSON

type FakeTracer = {}
type FakeBasicTracerProvider = {}
type FakeAttribute = {}

export function instantiatePullflowTracer(context: ExtensionContext) {
const { isTelemetryEnabled } = Store.get(context)
if (isTelemetryEnabled) {
return new Trace(context)
} else {
return new FakeTrace(context)
}
}
class FakeTrace {
tracer: FakeTracer
provider: FakeBasicTracerProvider
defaultAttributes: FakeAttribute

constructor(_context: ExtensionContext) {
this.provider = {}
this.tracer = {}
this.defaultAttributes = {}
}

dispose(): void {}
start({}: { name: string; attributes?: TraceAttributes }) {}
end({}: { attributes?: TraceAttributes }) {}
}
class Trace {
tracer: Tracer
provider: BasicTracerProvider
defaultAttributes: Attributes
span: Span | undefined

constructor(context: ExtensionContext) {
this.provider = new BasicTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: extensionInfo.name,
[SemanticResourceAttributes.SERVICE_VERSION]: extensionInfo.version,
}),
})

const exporter = new OTLPTraceExporter({
url: AppConfig.pullflow.telemetryUrl + '/v1/traces',
compression: 'gzip' as any,
})
this.provider.addSpanProcessor(new BatchSpanProcessor(exporter))
this.tracer = this.provider.getTracer(extensionInfo.name)
const { user } = Store.get(context)
this.defaultAttributes = user || {}
this.span = undefined
}

dispose(): void {
void this.provider.shutdown()
}

start({ name, attributes }: { name: string; attributes?: TraceAttributes }) {
this.span = this.tracer.startSpan(name, {
kind: SpanKind.INTERNAL,
startTime: Date.now(),
})
if (attributes)
this.span.setAttributes({
...attributes,
...this.defaultAttributes,
})
}

end({ attributes }: { attributes?: TraceAttributes }): void {
if (this.span === undefined) {
log.warn('span is undefined', 'trace.ts')
return
}
if (attributes)
this.span.setAttributes({
...attributes,
...this.defaultAttributes,
})
this.span.end(Date.now())
this.span = undefined
}
}
8 changes: 8 additions & 0 deletions src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export type CacheObject = {
keyStrokeCount?: number
lastKeyStrokeTime?: number | null
previousPresenceStatus?: PresenceStatus
isTelemetryEnabled?: boolean
isFlowDetectionEnabled?: boolean
}
export enum StatusBarState {
Loading,
Expand Down Expand Up @@ -169,3 +171,9 @@ export type RepoInfo = {
export type RefreshCodeReviewVariables = {
codeReviewId: string
}
export type TraceAttributes = QuickPickTrace

export type QuickPickTrace = {
title: string
selectedItem?: string
}
1 change: 1 addition & 0 deletions src/views/quickpicks/pullRequestActionPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export const pullRequestActionPicker = ({
})
}
QuickPick.create({
context,
items: menuItems,
title: 'Code Review Actions',
placeholder: 'Select action',
Expand Down
1 change: 1 addition & 0 deletions src/views/quickpicks/pullRequestUserPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const pullRequestUserPicker = ({
statusBar: StatusBarItem
}) => {
QuickPick.create({
context,
items: reviewerItems,
title: 'Reviewers',
placeholder: 'Select reviewer',
Expand Down
33 changes: 30 additions & 3 deletions src/views/quickpicks/quickPick.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
import { QuickPickItem, window } from 'vscode'
import { ExtensionContext, QuickPickItem, window } from 'vscode'
import { instantiatePullflowTracer } from '../../utils/trace'

export const QuickPick = {
create: <Type extends QuickPickItem>({
context,
items,
title,
placeholder,
onDidChangeSelection,
}: {
context: ExtensionContext
items: Type[]
title: string
placeholder: string
onDidChangeSelection: (selection: readonly Type[]) => void
}) => {
const quickPick = window.createQuickPick<Type>()

const trace = instantiatePullflowTracer(context)
trace.start({
name: title,
})

quickPick.items = items
quickPick.title = title
quickPick.placeholder = placeholder
quickPick.onDidChangeSelection(onDidChangeSelection)
quickPick.onDidHide(() => quickPick.dispose())
quickPick.onDidAccept(() => quickPick.dispose())

quickPick.onDidHide(() => {
trace.end({
attributes: {
title,
},
})
quickPick.dispose()
})

quickPick.onDidAccept(() => {
trace.end({
attributes: {
title,
selectedItem: quickPick.selectedItems[0]?.label,
},
})
quickPick.dispose()
})

quickPick.show()
},
}
1 change: 1 addition & 0 deletions src/views/quickpicks/spaceUserPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const spaceUserPicker = ({
}
})
QuickPick.create({
context,
title,
items: spaceUsersItems,
placeholder,
Expand Down
Loading

0 comments on commit c773eae

Please sign in to comment.