Thank you for your interest in contributing to Codehog, the PostHog extension for VS Code. This guide covers everything you need to get started.
- Node.js 20+
- pnpm (package manager)
- VS Code (latest stable)
- A PostHog account (US Cloud, EU Cloud, or self-hosted)
git clone https://github.com/PostHog/posthog-vscode.git
cd posthog-vscode
pnpm install
pnpm compilePress F5 in VS Code to launch the Extension Development Host OR open the "Run and Debug" panel. This opens a new VS Code window with the extension loaded from source.
pnpm compile # Build with webpack
pnpm watch # Build in watch mode (auto-recompile on save)
pnpm package # Production build with hidden source maps
pnpm lint # Run ESLint
pnpm test # Run tests in VS Code hostThe extension follows a three-layer architecture with a central orchestrator. See CLAUDE.md for the full file tree and code patterns.
The data layer. Services handle API calls, caching, authentication, and AST parsing. They have no VS Code UI dependencies beyond storage APIs.
- authService -- credentials via SecretStorage and Memento
- postHogService -- all PostHog REST and HogQL API calls
- treeSitterService -- AST parsing via web-tree-sitter for code intelligence
- flagCacheService / eventCacheService / experimentCacheService -- in-memory caches with listener-based change notification
- staleFlagService -- scans the workspace for stale flag references
- telemetryService -- extension telemetry via PostHog
VS Code language features: completions, decorations, code actions, code lenses, document links, and tree views. Each provider receives its dependencies (caches, tree-sitter) via constructor parameters.
Command Palette actions. Each file exports a registration function that binds command IDs (from constants.ts) to handler logic.
Webview panels for the sidebar and detail views. HTML is assembled from template literal functions in views/webview/ (styles, layout, script) and composed with a CSP nonce.
The orchestrator. Constructs all services, providers, and commands in activate(), wires dependencies, and pushes disposables to context.subscriptions.
All caches follow the same shape:
API response --> cache.update(items) --> listeners fire --> providers re-render
A cache service holds an array of items, a lookup method, an update() method that replaces the array and notifies listeners, and an onChange() method for subscriptions.
When adding a new cache, follow the existing pattern in flagCacheService.ts.
All decoration providers:
- Accept a cache service and
TreeSitterServicein the constructor - Register listeners for editor changes, document changes, and cache changes
- Debounce updates at 200ms
- Use
treeSitterService.findPostHogCalls()to locate relevant calls - Filter by a provider-local
METHOD_SET(e.g.,FLAG_METHODSorCAPTURE_METHODS) - Render via
renderOptions.afterwithcontentText,color, andfontStyle: 'italic'
- Guard:
treeSitterService.isSupported() - Get context:
treeSitterService.getCompletionContext(doc, pos) - Guard:
ctx.typematches the expected completion type - Return
CompletionItem[]built from cache data
All API calls go through postHogService.request<T>(path, options?).
- REST endpoints use
/api/projects/{id}/or/api/environments/{id}/ - HogQL queries use
POST /api/environments/{id}/query/with{ kind: 'HogQLQuery', query } - Always use
escapeHogQLString()for user-supplied values in HogQL strings - For paginated endpoints, use a
while(nextPath)loop and parse thenextURL from the response
- Add the command ID to
src/constants.tsin theCommandsobject - Create or extend a file in
src/commands/ - Register the command in
src/extension.tsinsideactivate() - Add the command entry to
contributes.commandsinpackage.json
- Add the setting definition to
contributes.configuration.propertiesinpackage.json - Read the setting with
vscode.workspace.getConfiguration('posthog').get('yourSetting')in the relevant provider or service
Tests run in the VS Code Extension Host via @vscode/test-electron.
pnpm testCode coverage is measured with c8. To run the test suite with coverage locally:
pnpm test:coverage # runs tests with coverage report
open coverage/index.html # view the HTML report in your browserThe HTML, lcov, and text reports are written to coverage/. Coverage is measured for src/services, src/providers, and src/utils. Tests, webview UI, and command/view glue are excluded.
The same script runs in CI on every PR and the coverage/ directory is uploaded as a build artifact for inspection.
src/test/extension.test.ts-- basic extension activation testsrc/test/cacheServices.test.ts-- tests for FlagCacheService, EventCacheService, and ExperimentCacheService (update, lookup, listener notification)src/test/codegenService.test.ts-- tests for code generation logicsrc/test/generateType.test.ts-- tests for type generationsrc/test/utils/hogql.test.ts-- tests forescapeHogQLString(injection prevention)src/test/utils/flagClassification.test.ts-- tests for flag type classification, rollout extraction, variant parsingsrc/test/utils/formatting.test.ts-- tests forformatCount,formatPct,buildBarsrc/test/utils/codeCleanup.test.ts-- tests forfindMatchingBrace,dedentBlock
Pure logic belongs in src/utils/ and is tested in src/test/utils/. Cache services are tested directly in src/test/. Keep tests focused on behavior, not implementation details.
Use Conventional Commits:
feat:-- new featurefix:-- bug fixci:-- CI/CD changesdocs:-- documentationchore:-- maintenance tasks
- All command IDs, view IDs, and storage keys live in
src/constants.ts - All PostHog API response shapes live in
src/models/types.ts
Each provider defines its own FLAG_METHODS Set. This is intentional -- do not refactor into a shared constant.
| Purpose | Hex | CSS Variable |
|---|---|---|
| Active | #4CBB17 |
--ph-green |
| Warning | #F9BD2B |
--ph-yellow |
| Brand | #1D4AFF |
--ph-blue |
| Error | #E53E3E |
--ph-red |
| Accent | #F54E00 |
--ph-orange |
For backgrounds, use VS Code theme variables (--vscode-*).
All decoration and highlight updates use a 200ms debounce.
- Startup cache loads:
.catch(() => {})(silent -- the UI shows empty state) - Service methods:
try/catchwithconsole.warn
The extension is instrumented with PostHog via TelemetryService. Telemetry is disabled in development mode (when running via F5) and respects the user's VS Code telemetry setting.
To capture a new event:
telemetry.capture("event_name", { key: "value" })Add telemetry events for meaningful user actions (sign in, flag toggle, scan initiated, etc.). Do not instrument internal implementation details.
This project uses changesets for version management and automated releases.
When you make a change that should be released, run:
pnpm changesetThis prompts you to select the change type (patch/minor/major) and write a summary. A markdown file is created in .changeset/ describing your change.
- patch — bug fixes, small improvements, documentation
- minor — new features, non-breaking changes
- major — breaking changes
When PRs with changesets are merged to main, the release workflow automatically:
- Checks if any changesets are pending
- Runs the full test suite (release gate — see below)
- Consumes all pending changesets
- Bumps the version in
package.json - Updates
CHANGELOG.mdwith the changeset summaries - Commits the version bump to
main - Creates a git tag (
v{version}) - Creates a GitHub Release with the changelog
- Builds and packages the extension
- Publishes to both VS Code Marketplace and Open VSX Registry
Do not manually bump the version in package.json. The CI pipeline handles it.
Publishing is gated by the test suite. No extension is ever published — to either marketplace, automatically or manually — unless the full test suite passes and coverage thresholds are met.
The gate is implemented as a reusable workflow (.github/workflows/test.yml) that other workflows call:
| Trigger | Test gate |
|---|---|
PR to main or push to feat/** |
test.yml runs directly |
Push to main with changesets (auto release) |
release.yml runs test.yml first; version bump, build, and publish only run if tests pass |
Manual publish-vscode.yml workflow_dispatch |
test.yml runs first; publish only runs if tests pass |
Manual publish-ovsx.yml workflow_dispatch |
test.yml runs first; publish only runs if tests pass |
Internal call from release.yml to publish-* |
Tests skipped (skip_tests: true) — already gated by release.yml |
In short: there is no path to a published version of the extension that bypasses the test suite. If a test fails, every downstream job (version bump, build, package, publish) is blocked automatically.
The release/publish gate also enforces minimum code coverage. The current thresholds are intentionally low (lines ≥ 30%, functions ≥ 30%) and are configured in .github/workflows/test.yml under MIN_LINES/MIN_FUNCTIONS. Ratchet them upward as test coverage improves.
- Use the pull request template when opening a PR
- Keep PRs focused on a single change
- Include before/after screenshots for UI changes
- Ensure
pnpm compileandpnpm testpass before submitting - Use conventional commit messages in the PR title