Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Epic: Telemetry for extension #53

Merged
merged 31 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ce25cae
added telemetry class
Jan 7, 2024
707b818
added TraceAttributes type
Jan 7, 2024
62ab8f3
fix: renamed class to trace
Jan 9, 2024
47db9c2
version bump
Jan 9, 2024
502914d
added tracing for quick picks
Jan 7, 2024
59531e8
added attributes for span end
Jan 7, 2024
3725950
updated object creation
Jan 9, 2024
9fde50f
fixed specs
Jan 9, 2024
c672a0a
fix: added telemetry settings in package.json
Jan 9, 2024
2a4b675
added isTelemetryEnabled in types.ts
Jan 9, 2024
026b3b1
fix: added code to initialize telemetry
Jan 9, 2024
65dd52f
updated telemetry url in config
srzainab Jan 15, 2024
921ae27
Merge pull request #52 from pullflow/feature/tracer-initialization
srzainab Jan 15, 2024
088f2b6
Merge branch 'epic/telemetry' of github.com:pullflow/vscode-pullflow …
srzainab Jan 15, 2024
952476c
Merge pull request #54 from pullflow/feature/quickpick-telemetry
srzainab Jan 15, 2024
22bb684
remove unneeded command for telemetry
srzainab Jan 16, 2024
4fa9b44
Merge branch 'epic/telemetry' of github.com:pullflow/vscode-pullflow …
srzainab Jan 16, 2024
f7a3fa8
added null object design pattern to avoid checking telemetry enabled …
srzainab Jan 16, 2024
959de8b
reset span
srzainab Jan 16, 2024
fb99a21
fixed spelling
srzainab Jan 16, 2024
9c5cc20
Merge branch 'feature/disable-telemetry' of github.com:pullflow/vscod…
srzainab Jan 16, 2024
b3c4ba4
update trackUserFlow to return dispose methods
srzainab Jan 16, 2024
456b336
added config for disabling auto flow detection
srzainab Jan 17, 2024
f6efb62
removed comments
srzainab Jan 17, 2024
d130515
Merge branch 'feature/disable-telemetry' of github.com:pullflow/vscod…
srzainab Jan 17, 2024
93f98ee
Updated text for markdown description
srzainab Jan 17, 2024
c169176
Opt out for flow status
srzainab Jan 18, 2024
31fc0d3
Merge pull request #59 from pullflow/srzainab/issue56
srzainab Jan 18, 2024
70e5204
removed unneeded console.log
srzainab Jan 24, 2024
2e1f2a2
Merge branch 'feature/disable-telemetry' of github.com:pullflow/vscod…
srzainab Jan 24, 2024
bc06a40
Merge pull request #55 from pullflow/feature/disable-telemetry
srzainab Jan 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading