Compare commits

...

3 Commits

6 changed files with 129 additions and 23 deletions
+3
View File
@@ -13,4 +13,7 @@ service EnvService {
// Reads text from the system clipboard.
rpc clipboardReadText(cline.EmptyRequest) returns (cline.String);
// Returns a stable machine identifier for telemetry distinctId purposes.
rpc getMachineId(cline.EmptyRequest) returns (cline.String);
}
+10 -1
View File
@@ -7,6 +7,7 @@ import {
import { WebviewProvider } from "./core/webview"
import { Logger } from "./services/logging/Logger"
import { PostHogClientProvider } from "./services/posthog/PostHogClientProvider"
import { EmptyRequest } from "./shared/proto/cline/common"
import { WebviewProviderType } from "./shared/webview/types"
import "./utils/path" // necessary to have access to String.prototype.toPosix
@@ -23,7 +24,15 @@ import { getLatestAnnouncementId } from "./utils/announcements"
*/
export async function initialize(context: vscode.ExtensionContext): Promise<WebviewProvider> {
// Initialize PostHog client provider
const distinctId = context.globalState.get<string>("cline.distinctId")
let distinctId = context.globalState.get<string>("cline.distinctId")
if (!distinctId) {
try {
const response = await HostProvider.env.getMachineId(EmptyRequest.create({}))
distinctId = response.value
} catch (e) {
// ignore; PostHogProvider will fall back to uuid
}
}
PostHogClientProvider.getInstance(distinctId)
// Migrate custom instructions to global Cline rules (one-time cleanup)
+13
View File
@@ -61,6 +61,19 @@ export class HostProvider {
logToChannel,
getCallbackUri,
)
// If telemetry was created early, update its machineId now that hostbridge is ready
try {
const { PostHogClientProvider } = require("@/services/posthog/PostHogClientProvider")
if (PostHogClientProvider?.isInitialized?.()) {
PostHogClientProvider.getInstance().updateMachineIdAsync?.()
}
} catch (err) {
const msg = `[Telemetry] skipped PostHog update: ${String(err)}`
if (HostProvider.isInitialized()) {
HostProvider.get().logToChannel(msg)
}
}
return HostProvider.instance
}
+7
View File
@@ -0,0 +1,7 @@
import { EmptyRequest, String } from "@shared/proto/cline/common"
import * as vscode from "vscode"
export async function getMachineId(_: EmptyRequest): Promise<String> {
const id = vscode.env.machineId || ""
return String.create({ value: id })
}
+65 -5
View File
@@ -1,6 +1,8 @@
import { PostHog } from "posthog-node"
import { v4 as uuidv4 } from "uuid"
import * as vscode from "vscode"
import { HostProvider } from "@/hosts/host-provider"
import { EmptyRequest } from "@/shared/proto/cline/common"
import { posthogConfig } from "../../shared/services/config/posthog-config"
import type { ClineAccountUserInfo } from "../auth/AuthService"
import { ErrorService } from "../error/ErrorService"
@@ -20,11 +22,50 @@ export class PostHogClientProvider {
public static getInstance(id?: string): PostHogClientProvider {
if (!PostHogClientProvider._instance) {
PostHogClientProvider._instance = new PostHogClientProvider(id)
// Use provided ID or fallback to ENV_ID
const distinctId = id || ENV_ID
PostHogClientProvider._instance = new PostHogClientProvider(distinctId)
// Asynchronously try to update with machine ID if using fallback
if (!id) {
PostHogClientProvider._instance.updateMachineIdAsync()
}
}
return PostHogClientProvider._instance
}
public static isInitialized(): boolean {
return PostHogClientProvider._instance !== null
}
// Resolves when distinctId is updated from ENV_ID to a real value
private distinctIdReadyResolve?: () => void
private distinctIdReadyPromise!: Promise<void>
public whenDistinctIdReady(timeoutMs = 1500): Promise<boolean> {
if (this.distinctId !== ENV_ID) {
return Promise.resolve(true)
}
return Promise.race<boolean>([
this.distinctIdReadyPromise.then(() => true),
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), timeoutMs)),
])
}
public async updateMachineIdAsync(): Promise<void> {
try {
const response = await HostProvider.env.getMachineId(EmptyRequest.create({}))
if (response?.value) {
this.distinctId = response.value
this.distinctIdReadyResolve?.()
}
} catch (error) {
if (HostProvider.isInitialized()) {
HostProvider.get().logToChannel(`[Telemetry] failed to get machine ID: ${String(error)}`)
}
}
}
protected telemetrySettings: TelemetrySettings = {
cline: true,
host: true,
@@ -38,6 +79,10 @@ export class PostHogClientProvider {
public readonly error: ErrorService
private constructor(public distinctId = ENV_ID) {
// Setup readiness promise
this.distinctIdReadyPromise = new Promise<void>((resolve) => {
this.distinctIdReadyResolve = resolve
})
// Initialize PostHog client
this.client = new PostHog(posthogConfig.apiKey, {
host: posthogConfig.host,
@@ -65,6 +110,10 @@ export class PostHogClientProvider {
(flag: string) => this.client.getFeatureFlag(flag, this.distinctId),
(flag: string) => this.client.getFeatureFlagPayload(flag, this.distinctId),
)
if (this.distinctId !== ENV_ID) {
this.distinctIdReadyResolve?.()
}
}
private get isTelemetryEnabled(): boolean {
@@ -73,11 +122,22 @@ export class PostHogClientProvider {
/** Whether telemetry is currently enabled based on user and VSCode settings */
private get telemetryLevel(): TelemetrySettings["level"] {
if (!vscode?.env?.isTelemetryEnabled) {
return "off"
// VS Code host: honor VS Code's telemetry flag if it is explicitly false
if (typeof vscode?.env?.isTelemetryEnabled === "boolean") {
if (vscode.env.isTelemetryEnabled === false) {
return "off"
}
const config = vscode.workspace.getConfiguration("telemetry")
return config?.get<TelemetrySettings["level"]>("telemetryLevel") || "all"
}
// Non-VS Code hosts (e.g., IntelliJ): fall back to our plugin setting
try {
const clineConfig = vscode.workspace.getConfiguration("cline")
const setting = clineConfig?.get<string>("telemetrySetting")
return setting === "disabled" ? "off" : "all"
} catch {
return "all"
}
const config = vscode.workspace.getConfiguration("telemetry")
return config?.get<TelemetrySettings["level"]>("telemetryLevel") || "all"
}
public toggleOptIn(optIn: boolean): void {
@@ -104,8 +104,12 @@ export class TelemetryService {
* @param provider PostHogClientProvider instance for sending analytics events
*/
public constructor(private provider: PostHogClientProvider) {
this.capture({ event: TelemetryService.EVENTS.USER.TELEMETRY_ENABLED })
console.info("[TelemetryService] Initialized with PostHogClientProvider")
// Defer the first telemetry_enabled event just long enough to pick up the real distinctId
void this.provider.whenDistinctIdReady().then((ready) => {
if (ready) {
this.capture({ event: TelemetryService.EVENTS.USER.TELEMETRY_ENABLED })
}
})
}
/**
@@ -120,20 +124,28 @@ export class TelemetryService {
if (!vscode.env.isTelemetryEnabled) {
// Only show warning if user has opted in to Cline telemetry but VS Code telemetry is disabled
if (didUserOptIn) {
void HostProvider.window
.showMessage({
const isVsCodeHost = vscode?.env?.uriScheme === "vscode"
if (isVsCodeHost) {
void HostProvider.window
.showMessage({
type: ShowMessageType.WARNING,
message:
"Anonymous Cline error and usage reporting is enabled, but VSCode telemetry is disabled. To enable error and usage reporting for this extension, enable VSCode telemetry in settings.",
options: {
items: ["Open Settings"],
},
})
.then((response) => {
if (response.selectedOption === "Open Settings") {
void vscode.commands.executeCommand("workbench.action.openSettings", "telemetry.telemetryLevel")
}
})
} else {
void HostProvider.window.showMessage({
type: ShowMessageType.WARNING,
message:
"Anonymous Cline error and usage reporting is enabled, but VSCode telemetry is disabled. To enable error and usage reporting for this extension, enable VSCode telemetry in settings.",
options: {
items: ["Open Settings"],
},
})
.then((response) => {
if (response.selectedOption === "Open Settings") {
void vscode.commands.executeCommand("workbench.action.openSettings", "telemetry.telemetryLevel")
}
message: "Anonymous Cline error and usage reporting is enabled, but host telemetry is disabled.",
})
}
}
}
@@ -154,14 +166,16 @@ export class TelemetryService {
*/
public capture(event: { event: string; properties?: unknown }): void {
const propertiesWithVersion = this.addProperties(event.properties)
// Use the provider's log method instead of direct client capture
this.provider.log(event.event, propertiesWithVersion)
}
public captureExtensionActivated() {
// Use provider's log method for the activation event
this.provider.log(TelemetryService.EVENTS.USER.EXTENSION_ACTIVATED)
void this.provider.whenDistinctIdReady().then((ready) => {
if (ready) {
this.capture({ event: TelemetryService.EVENTS.USER.EXTENSION_ACTIVATED })
}
})
}
/**