Skip to content

Commit 72bd131

Browse files
feat: Added .hai.config to support external overrides for telemetry configuration (#156)
1 parent 0dcb340 commit 72bd131

File tree

13 files changed

+163
-55
lines changed

13 files changed

+163
-55
lines changed

.changeset/new-knives-reflect.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hai-build-code-generator": minor
3+
---
4+
5+
Add .hai.config to support external overrides for telemetry configuration

src/core/controller/index.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ import { deleteFromContextDirectory } from "@utils/delete-helper"
7171
import { isLocalMcp, getLocalMcpDetails, getLocalMcp, getAllLocalMcps } from "@utils/local-mcp-registry"
7272
import { getStarCount } from "../../services/github/github"
7373
import { openFile } from "@integrations/misc/open-file"
74+
import { posthogClientProvider } from "@/services/posthog/PostHogClientProvider"
7475

7576
/*
7677
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -263,7 +264,6 @@ export class Controller {
263264
await this.postStateToWebview()
264265
break
265266
case "webviewDidLaunch":
266-
await this.updateHaiRulesState()
267267
this.postStateToWebview()
268268
this.workspaceTracker?.populateFilePaths() // don't await
269269
getTheme().then((theme) =>
@@ -742,9 +742,6 @@ export class Controller {
742742
ollamaEmbeddingModels,
743743
})
744744
break
745-
case "checkHaiRules":
746-
await this.updateHaiRulesState(true)
747-
break
748745
case "showToast":
749746
switch (message.toast?.toastType) {
750747
case "info":
@@ -1877,7 +1874,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
18771874
lastShownAnnouncementId,
18781875
customInstructions,
18791876
expertPrompt,
1880-
isHaiRulesPresent,
18811877
taskHistory,
18821878
autoApprovalSettings,
18831879
browserSettings,
@@ -1907,7 +1903,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
19071903
apiConfiguration,
19081904
customInstructions,
19091905
expertPrompt,
1910-
isHaiRulesPresent,
19111906
uriScheme: vscode.env.uriScheme,
19121907
currentTaskItem: this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined,
19131908
checkpointTrackerErrorMessage: this.task?.checkpointTrackerErrorMessage,
@@ -2512,19 +2507,13 @@ Commit message:`
25122507
}
25132508
}
25142509

2515-
async updateHaiRulesState(postToWebview: boolean = false) {
2516-
const workspaceFolder = getWorkspacePath()
2517-
if (!workspaceFolder) {
2518-
return
2519-
}
2520-
const haiRulesPath = path.join(workspaceFolder, GlobalFileNames.clineRules)
2521-
const isHaiRulePresent = await fileExistsAtPath(haiRulesPath)
2522-
2523-
await customUpdateState(this.context, "isHaiRulesPresent", isHaiRulePresent)
2510+
async updateTelemetryConfig() {
2511+
// Create new posthost client
2512+
posthogClientProvider.initPostHogClient()
25242513

2525-
if (postToWebview) {
2526-
await this.postStateToWebview()
2527-
}
2514+
// Update langfuse and posthog instance in telemetry
2515+
telemetryService.initPostHogClient()
2516+
telemetryService.initLangfuseClient()
25282517
}
25292518

25302519
async updateExpertPrompt(prompt?: string, expertName?: string) {

src/core/storage/state.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext, wor
212212
globalClineRulesToggles,
213213
requestTimeoutMs,
214214
shellIntegrationTimeout,
215-
isHaiRulesPresent,
216215
buildContextOptions,
217216
buildIndexProgress,
218217
isApiConfigurationValid,
@@ -314,7 +313,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext, wor
314313
customGetState(context, "globalClineRulesToggles") as Promise<ClineRulesToggles | undefined>,
315314
customGetState(context, "requestTimeoutMs") as Promise<number | undefined>,
316315
customGetState(context, "shellIntegrationTimeout") as Promise<number | undefined>,
317-
customGetState(context, "isHaiRulesPresent") as Promise<boolean | undefined>,
318316
customGetState(context, "buildContextOptions") as Promise<HaiBuildContextOptions | undefined>,
319317
customGetState(context, "buildIndexProgress") as Promise<HaiBuildIndexProgress | undefined>,
320318
customGetState(context, "isApiConfigurationValid") as Promise<boolean | undefined>,
@@ -469,7 +467,6 @@ export async function getAllExtensionState(context: vscode.ExtensionContext, wor
469467
lastShownAnnouncementId,
470468
customInstructions,
471469
expertPrompt,
472-
isHaiRulesPresent,
473470
taskHistory,
474471
buildContextOptions: buildContextOptions
475472
? {

src/global-constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export const GlobalFileNames = {
77
clineRules: ".hairules",
88
experts: ".hai-experts",
99
defaultExperts: "src/experts",
10+
haiConfig: ".hai.config",
1011
}

src/integrations/workspace/HaiFileSystemWatcher.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class HaiFileSystemWatcher {
2828
this.ig.add(
2929
content
3030
.split("\n")
31-
.filter((line) => line.trim() && !line.startsWith("#") && !line.includes(GlobalFileNames.clineRules)),
31+
.filter((line) => line.trim() && !line.startsWith("#") && !line.includes(GlobalFileNames.haiConfig)),
3232
)
3333
} catch (error) {
3434
console.log("HaiFileSystemWatcher No .gitignore found, using default exclusions.")
@@ -67,9 +67,9 @@ class HaiFileSystemWatcher {
6767
this.watcher.on("unlink", (filePath) => {
6868
console.log("HaiFileSystemWatcher File deleted", filePath)
6969

70-
// Check for .hairules
71-
if (this.isHaiRulesPath(filePath)) {
72-
this.providerRef.deref()?.updateHaiRulesState(true)
70+
// Check for .hai.config
71+
if (this.isHaiConfigPath(filePath)) {
72+
this.providerRef.deref()?.updateTelemetryConfig()
7373
}
7474

7575
// Check for the experts
@@ -83,9 +83,9 @@ class HaiFileSystemWatcher {
8383
this.watcher.on("add", (filePath) => {
8484
console.log("HaiFileSystemWatcher File added", filePath)
8585

86-
// Check for .hairules
87-
if (this.isHaiRulesPath(filePath)) {
88-
this.providerRef.deref()?.updateHaiRulesState(true)
86+
// Check for .hai.config
87+
if (this.isHaiConfigPath(filePath)) {
88+
this.providerRef.deref()?.updateTelemetryConfig()
8989
}
9090

9191
// Check for the experts
@@ -98,6 +98,12 @@ class HaiFileSystemWatcher {
9898

9999
this.watcher.on("change", (filePath) => {
100100
console.log("HaiFileSystemWatcher File changes", filePath)
101+
102+
// Check for .hai.config
103+
if (this.isHaiConfigPath(filePath)) {
104+
this.providerRef.deref()?.updateTelemetryConfig()
105+
}
106+
101107
// Check for the experts
102108
if (filePath.includes(GlobalFileNames.experts)) {
103109
this.providerRef.deref()?.loadExperts()
@@ -106,11 +112,11 @@ class HaiFileSystemWatcher {
106112
})
107113
}
108114

109-
isHaiRulesPath(path: string) {
115+
isHaiConfigPath(path: string) {
110116
const pathSplit = path.split(this.sourceFolder)
111117
const hairulesPath = pathSplit.length === 2 ? pathSplit[1].replace("/", "") : ""
112118

113-
return hairulesPath === GlobalFileNames.clineRules
119+
return hairulesPath === GlobalFileNames.haiConfig
114120
}
115121

116122
async dispose() {

src/services/posthog/PostHogClientProvider.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import { PostHog } from "posthog-node"
22
import { posthogConfig } from "@/shared/services/config/posthog-config"
3+
import { HaiConfig } from "@/shared/hai-config"
34

4-
class PostHogClientProvider {
5+
export class PostHogClientProvider {
56
private static instance: PostHogClientProvider
67
private client: PostHog
78

89
private constructor() {
9-
this.client = new PostHog(posthogConfig.apiKey, {
10-
host: posthogConfig.host,
10+
this.client = this.initPostHogClient()
11+
}
12+
13+
public initPostHogClient() {
14+
const config = HaiConfig.getPostHogConfig()
15+
const apiKey = config && config.apiKey ? config.apiKey : posthogConfig.apiKey
16+
const host = config && config.url ? config.url : posthogConfig.host
17+
18+
this.client = new PostHog(apiKey, {
19+
host,
1120
enableExceptionAutocapture: false,
1221
})
22+
23+
return this.client
1324
}
1425

1526
public static getInstance(): PostHogClientProvider {

src/services/posthog/telemetry/TelemetryService.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { BrowserSettings } from "@shared/BrowserSettings"
66
import { posthogClientProvider } from "../PostHogClientProvider"
77
import { getGitUserInfo } from "@utils/git"
88
import { Langfuse, LangfuseTraceClient } from "langfuse"
9+
import { HaiConfig } from "@/shared/hai-config"
910

1011
/**
1112
* PostHogClient handles telemetry event tracking for the Cline extension
@@ -122,25 +123,10 @@ class PostHogClient {
122123
* Initializes PostHog client with configuration
123124
*/
124125
private constructor() {
125-
this.client = posthogClientProvider.getClient()
126-
127-
// Set distinct ID for the client & identify the user
128-
this.client.identify({
129-
distinctId: this.distinctId,
130-
properties: {
131-
name: this.gitUserInfo.username,
132-
email: this.gitUserInfo.email,
133-
},
134-
})
126+
this.client = this.initPostHogClient()
135127

136128
// Initialize Langfuse client
137-
this.langfuse = new Langfuse({
138-
secretKey: process.env.LANGFUSE_API_KEY!,
139-
publicKey: process.env.LANGFUSE_PUBLIC_KEY!,
140-
baseUrl: process.env.LANGFUSE_API_URL,
141-
requestTimeout: 10000,
142-
enabled: true,
143-
})
129+
this.langfuse = this.initLangfuseClient()
144130
}
145131

146132
private createLangfuseTraceClient(taskId: string, isNew: boolean = false) {
@@ -159,6 +145,38 @@ class PostHogClient {
159145
})
160146
}
161147

148+
public initPostHogClient() {
149+
this.client = posthogClientProvider.getClient()
150+
151+
// Set distinct ID for the client & identify the user
152+
this.client.identify({
153+
distinctId: this.distinctId,
154+
properties: {
155+
name: this.gitUserInfo.username,
156+
email: this.gitUserInfo.email,
157+
},
158+
})
159+
160+
return this.client
161+
}
162+
163+
public initLangfuseClient() {
164+
const config = HaiConfig.getLangfuseConfig()
165+
const secretKey = config && config.apiKey ? config.apiKey : process.env.LANGFUSE_API_KEY!
166+
const publicKey = config && config.publicKey ? config.publicKey : process.env.LANGFUSE_PUBLIC_KEY!
167+
const baseUrl = config && config.apiUrl ? config.apiUrl : process.env.LANGFUSE_API_URL
168+
169+
this.langfuse = new Langfuse({
170+
secretKey,
171+
publicKey,
172+
baseUrl,
173+
requestTimeout: 10000,
174+
enabled: true,
175+
})
176+
177+
return this.langfuse
178+
}
179+
162180
/**
163181
* Updates the telemetry state based on user preferences and VSCode settings
164182
* Only enables telemetry if both VSCode global telemetry is enabled and user has opted in

src/shared/ExtensionMessage.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ export interface ExtensionState {
170170
telemetrySetting: TelemetrySetting
171171
shellIntegrationTimeout: number
172172
uriScheme?: string
173-
isHaiRulesPresent?: boolean
174173
userInfo?: {
175174
displayName: string | null
176175
email: string | null

src/shared/WebviewMessage.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ export interface WebviewMessage {
8686
| "searchCommits"
8787
| "showMcpView"
8888
| "fetchLatestMcpServersFromHub"
89-
| "checkHaiRules"
9089
| "telemetrySetting"
9190
| "openSettings"
9291
| "updateMcpTimeout"

src/shared/hai-config.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as fs from "fs"
2+
import * as path from "path"
3+
import { z } from "zod"
4+
import { GlobalFileNames } from "@/global-constants"
5+
import { getWorkspacePath } from "@/utils/path"
6+
7+
export const haiConfigSchema = z.object({
8+
name: z.string().optional(),
9+
langfuse: z
10+
.object({
11+
apiUrl: z.string().trim().optional(),
12+
apiKey: z.string().trim().optional(),
13+
publicKey: z.string().trim().optional(),
14+
})
15+
.optional(),
16+
posthog: z
17+
.object({
18+
url: z.string().trim().optional(),
19+
apiKey: z.string().trim().optional(),
20+
})
21+
.optional(),
22+
})
23+
24+
export class HaiConfig {
25+
static getConfig(workspacePath?: string) {
26+
if (!workspacePath) {
27+
workspacePath = getWorkspacePath()
28+
}
29+
30+
// Parse hai config file
31+
const configPath = path.join(workspacePath, GlobalFileNames.haiConfig)
32+
if (!fs.existsSync(configPath)) {
33+
console.log(`[HaiConfig]: ${configPath} does not exist`)
34+
return
35+
}
36+
37+
const content = fs.readFileSync(configPath, "utf-8")
38+
const lines = content.split("\n")
39+
40+
const config: Record<string, any> = {}
41+
42+
for (const line of lines) {
43+
const trimmed = line.trim()
44+
if (!trimmed || trimmed.startsWith("#")) {
45+
continue
46+
}
47+
48+
const [key, ...rest] = trimmed.split("=")
49+
const value = rest.join("=").trim() // supports '=' in value
50+
51+
const keyParts = key.trim().split(".")
52+
53+
let current = config
54+
for (let i = 0; i < keyParts.length; i++) {
55+
const part = keyParts[i]
56+
if (i === keyParts.length - 1) {
57+
current[part] = value
58+
} else {
59+
if (!current[part]) {
60+
current[part] = {}
61+
}
62+
current = current[part]
63+
}
64+
}
65+
}
66+
67+
// Validate the parsed content with schema
68+
const { success, data, error } = haiConfigSchema.safeParse(config)
69+
if (!success) {
70+
console.error(`Error validating ${configPath}, Error: ${error}`)
71+
return
72+
}
73+
74+
return data
75+
}
76+
77+
static getPostHogConfig(workspacePath?: string) {
78+
const config = HaiConfig.getConfig(workspacePath)
79+
return config?.posthog
80+
}
81+
82+
static getLangfuseConfig(workspacePath?: string) {
83+
const config = HaiConfig.getConfig(workspacePath)
84+
return config?.langfuse
85+
}
86+
}

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
2727
version,
2828
customInstructions,
2929
setCustomInstructions,
30-
isHaiRulesPresent,
3130
buildContextOptions,
3231
setBuildContextOptions,
3332
buildIndexProgress,

webview-ui/src/components/welcome/WelcomeView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const WelcomeView = () => {
1616
const handleSubmit = () => {
1717
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
1818
vscode.postMessage({ type: "embeddingConfiguration", embeddingConfiguration })
19-
vscode.postMessage({ type: "checkHaiRules" })
2019
}
2120

2221
return (

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ export const ExtensionStateContextProvider: React.FC<{
6767
version: "",
6868
clineMessages: [],
6969
taskHistory: [],
70-
isHaiRulesPresent: false,
7170
shouldShowAnnouncement: false,
7271
autoApprovalSettings: DEFAULT_AUTO_APPROVAL_SETTINGS,
7372
browserSettings: DEFAULT_BROWSER_SETTINGS,

0 commit comments

Comments
 (0)