Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 13 additions & 2 deletions packages/telemetry/src/BaseTelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ export abstract class BaseTelemetryClient implements TelemetryClient {
: !this.subscription.events.includes(eventName)
}

/**
* Determines if a specific property should be included in telemetry events
* Override in subclasses to filter specific properties
*/
protected isPropertyCapturable(_propertyName: string): boolean {
return true
}

protected async getEventProperties(event: TelemetryEvent): Promise<TelemetryEvent["properties"]> {
let providerProperties: TelemetryEvent["properties"] = {}
const provider = this.providerRef?.deref()

if (provider) {
try {
// Get the telemetry properties directly from the provider.
// Get properties from the provider
providerProperties = await provider.getTelemetryProperties()
} catch (error) {
// Log error but continue with capturing the event.
Expand All @@ -43,7 +51,10 @@ export abstract class BaseTelemetryClient implements TelemetryClient {

// Merge provider properties with event-specific properties.
// Event properties take precedence in case of conflicts.
return { ...providerProperties, ...(event.properties || {}) }
const mergedProperties = { ...providerProperties, ...(event.properties || {}) }

// Filter out properties that shouldn't be captured by this client
return Object.fromEntries(Object.entries(mergedProperties).filter(([key]) => this.isPropertyCapturable(key)))
}

public abstract capture(event: TelemetryEvent): Promise<void>
Expand Down
15 changes: 15 additions & 0 deletions packages/telemetry/src/PostHogTelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { BaseTelemetryClient } from "./BaseTelemetryClient"
export class PostHogTelemetryClient extends BaseTelemetryClient {
private client: PostHog
private distinctId: string = vscode.env.machineId
// Git repository properties that should be filtered out
private readonly gitPropertyNames = ["repositoryUrl", "repositoryName", "defaultBranch"]

constructor(debug = false) {
super(
Expand All @@ -26,6 +28,19 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
this.client = new PostHog(process.env.POSTHOG_API_KEY || "", { host: "https://us.i.posthog.com" })
}

/**
* Filter out git repository properties for PostHog telemetry
* @param propertyName The property name to check
* @returns Whether the property should be included in telemetry events
*/
protected override isPropertyCapturable(propertyName: string): boolean {
// Filter out git repository properties
if (this.gitPropertyNames.includes(propertyName)) {
return false
}
return true
}

public override async capture(event: TelemetryEvent): Promise<void> {
if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) {
if (this.debug) {
Expand Down
113 changes: 113 additions & 0 deletions packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,29 @@ describe("PostHogTelemetryClient", () => {
})
})

describe("isPropertyCapturable", () => {
it("should filter out git repository properties", () => {
const client = new PostHogTelemetryClient()

const isPropertyCapturable = getPrivateProperty<(propertyName: string) => boolean>(
client,
"isPropertyCapturable",
).bind(client)

// Git properties should be filtered out
expect(isPropertyCapturable("repositoryUrl")).toBe(false)
expect(isPropertyCapturable("repositoryName")).toBe(false)
expect(isPropertyCapturable("defaultBranch")).toBe(false)

// Other properties should be included
expect(isPropertyCapturable("appVersion")).toBe(true)
expect(isPropertyCapturable("vscodeVersion")).toBe(true)
expect(isPropertyCapturable("platform")).toBe(true)
expect(isPropertyCapturable("mode")).toBe(true)
expect(isPropertyCapturable("customProperty")).toBe(true)
})
})

describe("getEventProperties", () => {
it("should merge provider properties with event properties", async () => {
const client = new PostHogTelemetryClient()
Expand Down Expand Up @@ -112,6 +135,54 @@ describe("PostHogTelemetryClient", () => {
expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1)
})

it("should filter out git repository properties", async () => {
const client = new PostHogTelemetryClient()

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
// Git properties that should be filtered out
repositoryUrl: "https://github.com/example/repo",
repositoryName: "example/repo",
defaultBranch: "main",
}),
}

client.setProvider(mockProvider)

const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)

const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: {
customProp: "value",
},
})

// Git properties should be filtered out
expect(result).not.toHaveProperty("repositoryUrl")
expect(result).not.toHaveProperty("repositoryName")
expect(result).not.toHaveProperty("defaultBranch")

// Other properties should be included
expect(result).toEqual({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
customProp: "value",
})
})

it("should handle errors from provider gracefully", async () => {
const client = new PostHogTelemetryClient()

Expand Down Expand Up @@ -211,6 +282,48 @@ describe("PostHogTelemetryClient", () => {
}),
})
})

it("should filter out git repository properties when capturing events", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
// Git properties that should be filtered out
repositoryUrl: "https://github.com/example/repo",
repositoryName: "example/repo",
defaultBranch: "main",
}),
}

client.setProvider(mockProvider)

await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
})

expect(mockPostHogClient.capture).toHaveBeenCalledWith({
distinctId: "test-machine-id",
event: TelemetryEventName.TASK_CREATED,
properties: expect.objectContaining({
appVersion: "1.0.0",
test: "value",
}),
})

// Verify git properties are not included
const captureCall = mockPostHogClient.capture.mock.calls[0][0]
expect(captureCall.properties).not.toHaveProperty("repositoryUrl")
expect(captureCall.properties).not.toHaveProperty("repositoryName")
expect(captureCall.properties).not.toHaveProperty("defaultBranch")
})
})

describe("updateTelemetryState", () => {
Expand Down
12 changes: 3 additions & 9 deletions packages/types/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,7 @@ export const telemetryPropertiesSchema = z.object({
...gitPropertiesSchema.shape,
})

export const cloudTelemetryPropertiesSchema = z.object({
...telemetryPropertiesSchema.shape,
})

export type TelemetryProperties = z.infer<typeof telemetryPropertiesSchema>
export type CloudTelemetryProperties = z.infer<typeof cloudTelemetryPropertiesSchema>
export type GitProperties = z.infer<typeof gitPropertiesSchema>

/**
Expand Down Expand Up @@ -161,20 +156,20 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
TelemetryEventName.MODE_SETTINGS_CHANGED,
TelemetryEventName.CUSTOM_MODE_CREATED,
]),
properties: cloudTelemetryPropertiesSchema,
properties: telemetryPropertiesSchema,
}),
z.object({
type: z.literal(TelemetryEventName.TASK_MESSAGE),
properties: z.object({
...cloudTelemetryPropertiesSchema.shape,
...telemetryPropertiesSchema.shape,
taskId: z.string(),
message: clineMessageSchema,
}),
}),
z.object({
type: z.literal(TelemetryEventName.LLM_COMPLETION),
properties: z.object({
...cloudTelemetryPropertiesSchema.shape,
...telemetryPropertiesSchema.shape,
inputTokens: z.number(),
outputTokens: z.number(),
cacheReadTokens: z.number().optional(),
Expand All @@ -200,7 +195,6 @@ export type TelemetryEventSubscription =

export interface TelemetryPropertiesProvider {
getTelemetryProperties(): Promise<TelemetryProperties>
getCloudTelemetryProperties?(): Promise<CloudTelemetryProperties>
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import { webviewMessageHandler } from "./webviewMessageHandler"
import { WebviewMessage } from "../../shared/WebviewMessage"
import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
import { ProfileValidator } from "../../shared/ProfileValidator"
import { getWorkspaceGitInfo } from "../../utils/git"

/**
* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
Expand Down Expand Up @@ -1752,7 +1753,7 @@ export class ClineProvider
/**
* Returns properties to be included in every telemetry event
* This method is called by the telemetry service to get context information
* like the current mode, API provider, etc.
* like the current mode, API provider, git repository information, etc.
*/
public async getTelemetryProperties(): Promise<TelemetryProperties> {
const { mode, apiConfiguration, language } = await this.getState()
Expand All @@ -1772,6 +1773,10 @@ export class ClineProvider
this.log(`[getTelemetryProperties] Failed to get cloud auth state: ${error}`)
}

// Get git repository information
const gitInfo = await getWorkspaceGitInfo()

// Return all properties including git info - clients will filter as needed
return {
appName: packageJSON?.name ?? Package.name,
appVersion: packageJSON?.version ?? Package.version,
Expand All @@ -1785,6 +1790,7 @@ export class ClineProvider
diffStrategy: task?.diffStrategy?.getName(),
isSubtask: task ? !!task.parentTask : undefined,
cloudIsAuthenticated,
...gitInfo,
}
}
}
Loading
Loading