Skip to content

Commit 4911c9f

Browse files
committed
feat: track folder count in telemetry events for file operations
1 parent e308e38 commit 4911c9f

File tree

4 files changed

+260
-17
lines changed

4 files changed

+260
-17
lines changed

TELEMETRY.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ The following usage information is collected and reported:
1717
- When selections are added to Cody (excluding selected content)
1818
- When smart selections are added to Cody (excluding selected content)
1919

20-
#### File Counting
20+
#### File & Folder Counting
2121

22-
We track the total number of files added to Cody, but **we never read or store the actual contents of these files**. This helps us understand usage patterns without compromising your privacy. The count is purely numerical and anonymized.
22+
We track the total number of files & folders added to Cody, but **we never read or store the actual contents of these files**. This helps us understand usage patterns without compromising your privacy. The count is purely numerical and anonymized.
2323

2424
### Custom Commands
2525

src/commands/addToCody.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import * as path from 'path'
12
import * as vscode from 'vscode'
23
import { TELEMETRY_EVENTS } from '../constants/telemetry'
34
import { executeMentionFileCommand } from '../core/cody/commands'
45
import { formatFileTree, getWorkspaceFileTree } from '../core/filesystem/operations'
5-
import { getSelectedFileUris } from '../core/filesystem/processor'
6+
import { getSelectedFileUris, getSelectedFolderCount } from '../core/filesystem/processor'
67
import { createProvider } from '../core/llm'
78
import { createCompletionRequestMessages, parseLLMResponse } from '../core/llm/utils'
89
import { TelemetryService } from '../services/telemetry.service'
@@ -18,7 +19,8 @@ export async function addFile(folderUri: vscode.Uri) {
1819
)
1920

2021
telemetry.trackEvent(TELEMETRY_EVENTS.FILES.ADD_FILE, {
21-
fileCount
22+
fileCount,
23+
folderCount: 1 // Single file operation always has 1 folder
2224
})
2325
} catch (error: any) {
2426
vscode.window.showErrorMessage(`Failed to add file to Cody: ${error.message}`)
@@ -28,17 +30,19 @@ export async function addFile(folderUri: vscode.Uri) {
2830
export async function addSelection(folderUris: vscode.Uri[], recursive = false) {
2931
const telemetry = TelemetryService.getInstance()
3032
try {
31-
const files = await getSelectedFileUris(folderUris, {
33+
const { folderCount, fileUris } = await getSelectedFolderCount(folderUris, {
3234
recursive,
3335
progressTitle: 'Adding selection to Cody'
3436
})
35-
const fileCount = (await Promise.all(files.map(executeMentionFileCommand))).reduce(
37+
38+
const fileCount = (await Promise.all(fileUris.map(executeMentionFileCommand))).reduce(
3639
getSuccessCount,
3740
0
3841
)
3942

4043
telemetry.trackEvent(TELEMETRY_EVENTS.FILES.ADD_SELECTION, {
4144
fileCount,
45+
folderCount,
4246
recursive
4347
})
4448
} catch (error: any) {
@@ -49,17 +53,19 @@ export async function addSelection(folderUris: vscode.Uri[], recursive = false)
4953
export async function addFolder(folderUri: vscode.Uri, recursive = true) {
5054
const telemetry = TelemetryService.getInstance()
5155
try {
52-
const files = await getSelectedFileUris([folderUri], {
56+
const { folderCount, fileUris } = await getSelectedFolderCount([folderUri], {
5357
recursive,
5458
progressTitle: 'Adding folder to Cody'
5559
})
56-
const fileCount = (await Promise.all(files.map(executeMentionFileCommand))).reduce(
60+
61+
const fileCount = (await Promise.all(fileUris.map(executeMentionFileCommand))).reduce(
5762
getSuccessCount,
5863
0
5964
)
6065

6166
telemetry.trackEvent(TELEMETRY_EVENTS.FILES.ADD_FOLDER, {
6267
fileCount,
68+
folderCount,
6369
recursive
6470
})
6571
} catch (error: any) {
@@ -127,13 +133,22 @@ export async function addFilesSmart(folderUris: vscode.Uri[], context: vscode.Ex
127133
// Convert paths to URIs and add to Cody
128134
const selectedFileUris = selectedFiles.map(filePath => vscode.Uri.file(filePath))
129135

136+
// Count unique folders from the selected files
137+
const uniqueFolders = new Set<string>()
138+
selectedFiles.forEach(filePath => {
139+
const dirPath = path.dirname(filePath)
140+
uniqueFolders.add(dirPath)
141+
})
142+
const folderCount = uniqueFolders.size
143+
130144
progress.report({ increment: 15, message: 'Adding files to Cody...' })
131145
const fileCount = (
132146
await Promise.all(selectedFileUris.map(executeMentionFileCommand))
133147
).reduce(getSuccessCount, 0)
134148

135149
telemetry.trackEvent(TELEMETRY_EVENTS.FILES.ADD_SMART_SELECTION, {
136-
fileCount
150+
fileCount,
151+
folderCount
137152
})
138153

139154
// Provide feedback to the user.
@@ -151,9 +166,6 @@ export async function addFilesSmart(folderUris: vscode.Uri[], context: vscode.Ex
151166
modal: true
152167
}
153168
)
154-
telemetry.trackEvent(TELEMETRY_EVENTS.FILES.ADD_SMART_SELECTION, {
155-
fileCount
156-
})
157169
} catch (error: any) {
158170
vscode.window.showErrorMessage(`Failed to add files smart to Cody: ${error.message}`)
159171
throw error

src/core/filesystem/processor.ts

+124
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,127 @@ export async function getSelectedFileUris(
6969

7070
return fileUris
7171
}
72+
73+
/**
74+
* Gets both the selected file URIs and counts the number of folders processed
75+
* @param uris Input URIs to process
76+
* @param options Processing configuration options
77+
* @returns Object containing folder count and file URIs
78+
*/
79+
export async function getSelectedFolderCount(
80+
uris: vscode.Uri[],
81+
options: Partial<ProcessingConfig> = {}
82+
): Promise<{ folderCount: number; fileUris: vscode.Uri[] }> {
83+
if (uris.length === 0) {
84+
vscode.window.showWarningMessage('No files or folders are selected to process.')
85+
return { folderCount: 0, fileUris: [] }
86+
}
87+
88+
const config = getProcessingConfig(options)
89+
90+
// Count folders and collect file URIs
91+
let folderCount = 0
92+
const processedFolderPaths = new Set<string>()
93+
94+
// Count initial folders
95+
for (const uri of uris) {
96+
const stat = await vscode.workspace.fs.stat(uri)
97+
if (stat.type === vscode.FileType.Directory) {
98+
folderCount++
99+
processedFolderPaths.add(uri.fsPath)
100+
}
101+
}
102+
103+
// Track subfolders during collection if recursive
104+
const trackFolderCallback = (folderPath: string) => {
105+
if (!processedFolderPaths.has(folderPath)) {
106+
folderCount++
107+
processedFolderPaths.add(folderPath)
108+
}
109+
}
110+
111+
const fileUris = await collectFileUrisWithFolderTracking(
112+
uris,
113+
{
114+
recursive: config.recursive,
115+
excludedFileTypes: config.excludedFileTypes,
116+
excludedFolders: config.excludedFolders
117+
},
118+
trackFolderCallback
119+
)
120+
121+
const fileCount = fileUris.length
122+
const shouldProceed = await validateFileCount(fileCount, config.fileThreshold)
123+
if (!shouldProceed) {
124+
return { folderCount: 0, fileUris: [] }
125+
}
126+
127+
return { folderCount, fileUris }
128+
}
129+
130+
/**
131+
* Collects file URIs while tracking folder paths
132+
*/
133+
async function collectFileUrisWithFolderTracking(
134+
uris: vscode.Uri[],
135+
options: { recursive: boolean; excludedFileTypes: string[]; excludedFolders: string[] },
136+
folderCallback: (folderPath: string) => void
137+
): Promise<vscode.Uri[]> {
138+
const fileUris: vscode.Uri[] = []
139+
140+
for (const uri of uris) {
141+
const stat = await vscode.workspace.fs.stat(uri)
142+
if (stat.type === vscode.FileType.File) {
143+
const name = path.basename(uri.fsPath)
144+
if (!isFileTypeExcluded(name, options.excludedFileTypes)) {
145+
fileUris.push(uri)
146+
}
147+
} else if (stat.type === vscode.FileType.Directory) {
148+
const dirFileUris = await collectFileUrisFromDirectoryWithTracking(
149+
uri,
150+
options,
151+
folderCallback
152+
)
153+
fileUris.push(...dirFileUris)
154+
}
155+
}
156+
157+
return fileUris
158+
}
159+
160+
/**
161+
* Collects file URIs from a directory while tracking subfolder paths
162+
*/
163+
async function collectFileUrisFromDirectoryWithTracking(
164+
dirUri: vscode.Uri,
165+
options: { recursive: boolean; excludedFileTypes: string[]; excludedFolders: string[] },
166+
folderCallback: (folderPath: string) => void
167+
): Promise<vscode.Uri[]> {
168+
const fileUris: vscode.Uri[] = []
169+
const entries = await vscode.workspace.fs.readDirectory(dirUri)
170+
171+
for (const [name, type] of entries) {
172+
const entryUri = vscode.Uri.joinPath(dirUri, name)
173+
174+
if (type === vscode.FileType.File) {
175+
if (!isFileTypeExcluded(name, options.excludedFileTypes)) {
176+
fileUris.push(entryUri)
177+
}
178+
} else if (type === vscode.FileType.Directory && options.recursive) {
179+
if (!isFolderNameExcluded(name, options.excludedFolders)) {
180+
// Track this subfolder
181+
folderCallback(entryUri.fsPath)
182+
183+
// Process its contents
184+
const subFileUris = await collectFileUrisFromDirectoryWithTracking(
185+
entryUri,
186+
options,
187+
folderCallback
188+
)
189+
fileUris.push(...subFileUris)
190+
}
191+
}
192+
}
193+
194+
return fileUris
195+
}

src/services/__tests__/telemetry.service.test.ts

+112-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import * as assert from 'assert'
2+
import * as posthogModule from 'posthog-node'
23
import * as sinon from 'sinon'
34
import * as vscode from 'vscode'
5+
import { TELEMETRY_EVENTS } from '../../constants/telemetry'
46
import { TelemetryService } from '../telemetry.service'
57

68
suite('TelemetryService Tests', () => {
79
let sandbox: sinon.SinonSandbox
10+
let posthogCaptureStub: sinon.SinonStub
11+
let telemetryServiceInstance: TelemetryService
812

913
setup(() => {
1014
sandbox = sinon.createSandbox()
@@ -16,8 +20,31 @@ suite('TelemetryService Tests', () => {
1620
get: configGetStub
1721
} as any)
1822

19-
// Stub console.log to avoid test output pollution
23+
// Stub console methods to avoid test output pollution
2024
sandbox.stub(console, 'log')
25+
sandbox.stub(console, 'error')
26+
27+
// Create a fresh instance and clear any existing ones
28+
// @ts-ignore: Accessing private static instance
29+
TelemetryService.instance = undefined
30+
31+
// Set up PostHog stub
32+
posthogCaptureStub = sandbox.stub()
33+
const posthogStub = {
34+
capture: posthogCaptureStub
35+
}
36+
37+
// Stub the PostHog constructor
38+
sandbox.stub(posthogModule, 'PostHog').returns(posthogStub as any)
39+
40+
// Get the instance after stubbing
41+
telemetryServiceInstance = TelemetryService.getInstance()
42+
43+
// Set the service as ready and enabled
44+
// @ts-ignore: Accessing private properties
45+
telemetryServiceInstance.ready = true
46+
// @ts-ignore: Accessing private properties
47+
telemetryServiceInstance.enabled = true
2148
})
2249

2350
teardown(() => {
@@ -32,16 +59,96 @@ suite('TelemetryService Tests', () => {
3259
})
3360

3461
test('isEnabled should return the boolean state', () => {
35-
const telemetryService = TelemetryService.getInstance()
36-
const result = telemetryService.isEnabled()
62+
const result = telemetryServiceInstance.isEnabled()
3763

3864
// Just check that it returns a boolean value, as the actual
3965
// implementation details might change
4066
assert.strictEqual(typeof result, 'boolean')
4167
})
4268

4369
test('isReady should return ready state', () => {
44-
const telemetryService = TelemetryService.getInstance()
45-
assert.strictEqual(typeof telemetryService.isReady(), 'boolean')
70+
assert.strictEqual(typeof telemetryServiceInstance.isReady(), 'boolean')
71+
})
72+
73+
test('trackEvent should track file count and folder count for ADD_FILE event', () => {
74+
// Track event with file count and folder count
75+
telemetryServiceInstance.trackEvent(TELEMETRY_EVENTS.FILES.ADD_FILE, {
76+
fileCount: 5,
77+
folderCount: 1
78+
})
79+
80+
// Verify capture was called with correct parameters
81+
sinon.assert.calledOnce(posthogCaptureStub)
82+
const captureCall = posthogCaptureStub.getCall(0).args[0]
83+
84+
assert.strictEqual(captureCall.event, TELEMETRY_EVENTS.FILES.ADD_FILE)
85+
assert.strictEqual(captureCall.properties.fileCount, 5)
86+
assert.strictEqual(captureCall.properties.folderCount, 1)
87+
})
88+
89+
test('trackEvent should track file count and folder count for ADD_FOLDER event', () => {
90+
// Track event with file count, folder count, and recursive flag
91+
telemetryServiceInstance.trackEvent(TELEMETRY_EVENTS.FILES.ADD_FOLDER, {
92+
fileCount: 10,
93+
folderCount: 3,
94+
recursive: true
95+
})
96+
97+
// Verify capture was called with correct parameters
98+
sinon.assert.calledOnce(posthogCaptureStub)
99+
const captureCall = posthogCaptureStub.getCall(0).args[0]
100+
101+
assert.strictEqual(captureCall.event, TELEMETRY_EVENTS.FILES.ADD_FOLDER)
102+
assert.strictEqual(captureCall.properties.fileCount, 10)
103+
assert.strictEqual(captureCall.properties.folderCount, 3)
104+
assert.strictEqual(captureCall.properties.recursive, true)
105+
})
106+
107+
test('trackEvent should track file count and folder count for ADD_SELECTION event', () => {
108+
// Track event with file count, folder count, and recursive flag
109+
telemetryServiceInstance.trackEvent(TELEMETRY_EVENTS.FILES.ADD_SELECTION, {
110+
fileCount: 7,
111+
folderCount: 2,
112+
recursive: false
113+
})
114+
115+
// Verify capture was called with correct parameters
116+
sinon.assert.calledOnce(posthogCaptureStub)
117+
const captureCall = posthogCaptureStub.getCall(0).args[0]
118+
119+
assert.strictEqual(captureCall.event, TELEMETRY_EVENTS.FILES.ADD_SELECTION)
120+
assert.strictEqual(captureCall.properties.fileCount, 7)
121+
assert.strictEqual(captureCall.properties.folderCount, 2)
122+
assert.strictEqual(captureCall.properties.recursive, false)
123+
})
124+
125+
test('trackEvent should track file count and folder count for ADD_SMART_SELECTION event', () => {
126+
// Track event with file count and folder count
127+
telemetryServiceInstance.trackEvent(TELEMETRY_EVENTS.FILES.ADD_SMART_SELECTION, {
128+
fileCount: 15,
129+
folderCount: 4
130+
})
131+
132+
// Verify capture was called with correct parameters
133+
sinon.assert.calledOnce(posthogCaptureStub)
134+
const captureCall = posthogCaptureStub.getCall(0).args[0]
135+
136+
assert.strictEqual(captureCall.event, TELEMETRY_EVENTS.FILES.ADD_SMART_SELECTION)
137+
assert.strictEqual(captureCall.properties.fileCount, 15)
138+
assert.strictEqual(captureCall.properties.folderCount, 4)
139+
})
140+
141+
test('trackEvent should not capture when telemetry is disabled', () => {
142+
// Mock isEnabled to return false by setting the private enabled field
143+
// @ts-ignore: Accessing private properties
144+
telemetryServiceInstance.enabled = false
145+
146+
telemetryServiceInstance.trackEvent(TELEMETRY_EVENTS.FILES.ADD_FILE, {
147+
fileCount: 5,
148+
folderCount: 1
149+
})
150+
151+
// Verify capture was not called
152+
sinon.assert.notCalled(posthogCaptureStub)
46153
})
47154
})

0 commit comments

Comments
 (0)