Compare commits

...
Author SHA1 Message Date
John Choi 458d479c00 fix(webview): make PostHog feature flags reliable in the packaged webview
Two issues made webview feature flags unreliable in production/Nightly builds:

1. CSP blocked PostHog remote-config scripts. The production webview CSP
   allowed PostHog over connect-src (the /decide call) but script-src was only
   'nonce-...' 'unsafe-eval'. PostHog lazy-loads helper scripts (config.js,
   array.js, surveys.js, exception-autocapture.js) from data.cline.bot via
   injected <script> tags, which were all blocked -> flag resolution was
   unreliable. Add https://*.posthog.com https://*.cline.bot to script-src.

2. 'flags not loaded yet' was a false-negative. useHasFeatureFlag collapses
   PostHog's undefined-while-loading state into false, so a flag can briefly
   read as disabled during the auth/identify handshake. Add a tri-state
   useFeatureFlagStatus (enabled | disabled | loading) so callers that must
   not flash a disabled/empty state can distinguish loading from disabled.
2026-06-24 14:37:27 -07:00
2 changed files with 47 additions and 1 deletions
@@ -110,7 +110,7 @@ export abstract class WebviewProvider {
font-src ${this.getCspSource()} data:;
style-src ${this.getCspSource()} 'unsafe-inline';
img-src ${this.getCspSource()} https: data:;
script-src 'nonce-${nonce}' 'unsafe-eval';">
script-src 'nonce-${nonce}' 'unsafe-eval' https://*.posthog.com https://*.cline.bot;">
<title>Cline</title>
</head>
<body>
@@ -34,3 +34,49 @@ export const useHasFeatureFlag = (flagName: string): boolean => {
return flagEnabled
}
/**
* Tri-state feature flag status.
* - "enabled" / "disabled": PostHog has resolved the flag.
* - "loading": PostHog hasn't returned flags yet (e.g. mid auth/identify
* handshake, or while remote config is still loading).
*
* The boolean {@link useHasFeatureFlag} collapses "loading" into `false`, which
* causes a false-negative when a feature is briefly treated as disabled before
* flags have actually resolved. Callers that must not flash a disabled/empty
* state before flags load should use this variant and treat "loading" distinctly
* from "disabled".
*/
export type FeatureFlagStatus = "enabled" | "disabled" | "loading"
export const useFeatureFlagStatus = (flagName: string): FeatureFlagStatus => {
const { environment } = useExtensionState()
const isSelfHostedOrUnknown = !environment || environment === "selfHosted"
const [status, setStatus] = useState<FeatureFlagStatus>("loading")
useEffect(() => {
if (isSelfHostedOrUnknown) {
setStatus("disabled")
return
}
const readFlag = () => {
// posthog.isFeatureEnabled returns undefined until flags have loaded.
const value = posthog.isFeatureEnabled(flagName)
if (value === undefined) {
setStatus("loading")
return
}
setStatus(value ? "enabled" : "disabled")
}
readFlag()
return posthog.onFeatureFlags(readFlag)
}, [flagName, isSelfHostedOrUnknown])
if (isSelfHostedOrUnknown) {
return "disabled"
}
return status
}