You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds cross-file PostHog wrapper detection so the enricher can recognize and annotate calls to user-defined functions that wrap PostHog SDK methods, and fixes JSX comment rendering to use {/* */} syntax instead of // when calls appear inside JSX elements.
Problem
Previously, the enricher only detected direct PostHog SDK calls (e.g. posthog.capture(...)). If a codebase wrapped PostHog in a utility function like track(event) and imported it across files, those calls were invisible to the enricher. Additionally, inline comment annotations appended // [PostHog] ... suffixes unconditionally, which produces invalid syntax when the call site is inside a JSX tree.
Changes
Wrapper detection
Adds wrapper-detector.ts which analyzes function bodies to identify functions that directly call PostHog SDK methods, classifying them as either fixed-key (hardcoded event name) or pass-through (event name forwarded from a parameter). Supports JS/TS and Python.
Adds import-resolver.ts which resolves relative import specifiers to absolute file paths for JS/TS (with extension probing and index.* fallback) and Python (relative dot-prefix syntax and __init__.py packages).
file-enricher.ts now detects relative imports in a file, resolves wrappers from those imported modules, and passes a ParseContext (containing wrappersByLocalName and namespaceWrappers) into parse(). Files with no direct posthog literal but with resolvable wrappers are still enriched.
call-detector.ts consumes ParseContext to synthesize PostHogCall entries for bare and namespace wrapper calls, reusing the existing constant-map for identifier resolution.
Import query strings added to languages.ts for JS/TS and Python.
JSX-aware comment rendering
Adds isInsideJsx() helper in ast-helpers.ts that walks the AST upward to detect JSX element ancestry.
PostHogCall, CapturedEvent, FlagCheck, ListItem, and EnrichedListItem gain an inJsx field.
comment-formatter.ts now emits {/* [PostHog] ... */} for JSX-context items and handles mixed JSX/JS items on the same line by inserting a leading JSX-style block comment instead of an inline suffix.
Other fixes
FlagEvaluationStats now includes windowDays so the comment formatter can render the actual query window instead of a hardcoded 7d.
Flag evaluation stat fetch failures are surfaced as evaluationStatsError and rendered as "eval stats unavailable" rather than silently omitting the stat.
Flag URLs are now pre-computed in parse-result.ts (with trailing-slash normalization) and passed via flagUrls on EnrichmentContext, removing the host/projectId fields from that interface.
FunctionInfo gains bodyEndLine.
ParserManager uses a stable string key for query cache entries instead of lang.toString().
packages/enricher/src/enricher.ts, line 1215-1216 (link)
Uncaught parse error escapes getWrappersForFile
this.detector.findWrappers is not wrapped in a try/catch. If tree-sitter throws on a malformed source file, the error propagates through Promise.all inside buildWrapperContext (which has no outer try/catch for this section), then up through enrichFileForAgent's buildWrapperContext call — which is also outside the try/catch that guards deps.enricher.parse. The result is an unhandled rejection from enrichment rather than a graceful null return.
This is a comment left during a code review.
Path: packages/enricher/src/enricher.ts
Line: 1215-1216
Comment:
**Uncaught parse error escapes `getWrappersForFile`**`this.detector.findWrappers` is not wrapped in a try/catch. If tree-sitter throws on a malformed source file, the error propagates through `Promise.all` inside `buildWrapperContext` (which has no outer try/catch for this section), then up through `enrichFileForAgent`'s `buildWrapperContext` call — which is also outside the try/catch that guards `deps.enricher.parse`. The result is an unhandled rejection from enrichment rather than a graceful `null` return.
```typescripttry {
const wrappers =awaitthis.detector.findWrappers(source, languageId);
returnthis.setWrapperCache(absPath, mtimeMs, wrappers);
} catch {
returnthis.setWrapperCache(absPath, mtimeMs, []);
}
```
How can I resolve this? If you propose a fix, please make it concise.
packages/enricher/src/import-resolver.ts, line 1482-1488 (link)
Synchronous statSync blocks the event loop in an async function
isFile uses statSync, which is called up to ~16 times per import specifier (8 extension probes + 8 index-file probes) from within findImports (an async function). For a file with a dozen relative imports this adds up to ~200 blocking stat calls on the hot path. Consider switching to fs.promises.stat (or fs.promises.access) and caching probed paths, or at least documenting that this function is intentionally synchronous for simplicity.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/enricher/src/import-resolver.ts
Line: 1482-1488
Comment:
**Synchronous `statSync` blocks the event loop in an async function**`isFile` uses `statSync`, which is called up to ~16 times per import specifier (8 extension probes + 8 index-file probes) from within `findImports` (an `async` function). For a file with a dozen relative imports this adds up to ~200 blocking stat calls on the hot path. Consider switching to `fs.promises.stat` (or `fs.promises.access`) and caching probed paths, or at least documenting that this function is intentionally synchronous for simplicity.
How can I resolve this? If you propose a fix, please make it concise.
packages/enricher/src/enricher.ts, line 1241-1248 (link)
Cache eviction may fire when updating an existing entry
setWrapperCache evicts the oldest entry whenever size >= WRAPPER_CACHE_MAX, even if absPath is already in the map (i.e. the set call would be an in-place update rather than a net-new insertion). This can unnecessarily evict a perfectly valid unrelated entry. A simple guard fixes it:
This is a comment left during a code review.
Path: packages/enricher/src/enricher.ts
Line: 1241-1248
Comment:
**Cache eviction may fire when updating an existing entry**`setWrapperCache` evicts the oldest entry whenever `size >= WRAPPER_CACHE_MAX`, even if `absPath` is already in the map (i.e. the `set` call would be an in-place update rather than a net-new insertion). This can unnecessarily evict a perfectly valid unrelated entry. A simple guard fixes it:
```typescriptprivatesetWrapperCache(
absPath: string,
mtimeMs: number,
wrappers: LocalWrapper[],
): LocalWrapper[] {
if (!this.wrapperCache.has(absPath) &&this.wrapperCache.size>=WRAPPER_CACHE_MAX) {
const oldest =this.wrapperCache.keys().next().value;
if (oldest!==undefined) {
this.wrapperCache.delete(oldest);
}
}
this.wrapperCache.set(absPath, { mtimeMs, wrappers });
returnwrappers;
}
```
How can I resolve this? If you propose a fix, please make it concise.
packages/enricher/src/wrapper-integration.test.ts, line 2682-2722 (link)
buildContext duplicates the production buildWrapperContext logic
This helper re-implements the same import→wrapper-lookup pipeline as buildWrapperContext in file-enricher.ts, including the edge/namespace/default branching — but without the MAX_RELATIVE_IMPORTS bound. If the production logic changes (e.g. to handle re-exports or aliased namespaces), this copy might silently diverge. Exporting buildWrapperContext (or an inner helper) for reuse in tests would satisfy OnceAndOnlyOnce.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/enricher/src/wrapper-integration.test.ts
Line: 2682-2722
Comment:
**`buildContext` duplicates the production `buildWrapperContext` logic**
This helper re-implements the same import→wrapper-lookup pipeline as `buildWrapperContext` in `file-enricher.ts`, including the edge/namespace/default branching — but without the `MAX_RELATIVE_IMPORTS` bound. If the production logic changes (e.g. to handle re-exports or aliased namespaces), this copy might silently diverge. Exporting `buildWrapperContext` (or an inner helper) for reuse in tests would satisfy OnceAndOnlyOnce.
How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/enricher/src/enricher.ts
Line: 1215-1216
Comment:
**Uncaught parse error escapes `getWrappersForFile`**`this.detector.findWrappers` is not wrapped in a try/catch. If tree-sitter throws on a malformed source file, the error propagates through `Promise.all` inside `buildWrapperContext` (which has no outer try/catch for this section), then up through `enrichFileForAgent`'s `buildWrapperContext` call — which is also outside the try/catch that guards `deps.enricher.parse`. The result is an unhandled rejection from enrichment rather than a graceful `null` return.
```typescripttry {
const wrappers =awaitthis.detector.findWrappers(source, languageId);
returnthis.setWrapperCache(absPath, mtimeMs, wrappers);
} catch {
returnthis.setWrapperCache(absPath, mtimeMs, []);
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: packages/enricher/src/import-resolver.ts
Line: 1482-1488
Comment:
**Synchronous `statSync` blocks the event loop in an async function**`isFile` uses `statSync`, which is called up to ~16 times per import specifier (8 extension probes + 8 index-file probes) from within `findImports` (an `async` function). For a file with a dozen relative imports this adds up to ~200 blocking stat calls on the hot path. Consider switching to `fs.promises.stat` (or `fs.promises.access`) and caching probed paths, or at least documenting that this function is intentionally synchronous for simplicity.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: packages/enricher/src/enricher.ts
Line: 1241-1248
Comment:
**Cache eviction may fire when updating an existing entry**`setWrapperCache` evicts the oldest entry whenever `size >= WRAPPER_CACHE_MAX`, even if `absPath` is already in the map (i.e. the `set` call would be an in-place update rather than a net-new insertion). This can unnecessarily evict a perfectly valid unrelated entry. A simple guard fixes it:
```typescriptprivatesetWrapperCache(
absPath: string,
mtimeMs: number,
wrappers: LocalWrapper[],
): LocalWrapper[] {
if (!this.wrapperCache.has(absPath) &&this.wrapperCache.size>=WRAPPER_CACHE_MAX) {
const oldest =this.wrapperCache.keys().next().value;
if (oldest!==undefined) {
this.wrapperCache.delete(oldest);
}
}
this.wrapperCache.set(absPath, { mtimeMs, wrappers });
returnwrappers;
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: packages/enricher/src/wrapper-integration.test.ts
Line: 2682-2722
Comment:
**`buildContext` duplicates the production `buildWrapperContext` logic**
This helper re-implements the same import→wrapper-lookup pipeline as `buildWrapperContext` in `file-enricher.ts`, including the edge/namespace/default branching — but without the `MAX_RELATIVE_IMPORTS` bound. If the production logic changes (e.g. to handle re-exports or aliased namespaces), this copy might silently diverge. Exporting `buildWrapperContext` (or an inner helper) for reuse in tests would satisfy OnceAndOnlyOnce.
How can I resolve this? If you propose a fix, please make it concise.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TLDR
Adds cross-file PostHog wrapper detection so the enricher can recognize and annotate calls to user-defined functions that wrap PostHog SDK methods, and fixes JSX comment rendering to use
{/* */}syntax instead of//when calls appear inside JSX elements.Problem
Previously, the enricher only detected direct PostHog SDK calls (e.g.
posthog.capture(...)). If a codebase wrapped PostHog in a utility function liketrack(event)and imported it across files, those calls were invisible to the enricher. Additionally, inline comment annotations appended// [PostHog] ...suffixes unconditionally, which produces invalid syntax when the call site is inside a JSX tree.Changes
Wrapper detection
wrapper-detector.tswhich analyzes function bodies to identify functions that directly call PostHog SDK methods, classifying them as eitherfixed-key(hardcoded event name) orpass-through(event name forwarded from a parameter). Supports JS/TS and Python.import-resolver.tswhich resolves relative import specifiers to absolute file paths for JS/TS (with extension probing andindex.*fallback) and Python (relative dot-prefix syntax and__init__.pypackages).PostHogEnrichergainsgetWrappersForFile(absPath)(with mtime-based LRU cache) andfindImportsInSource(source, langId, callerAbsPath).file-enricher.tsnow detects relative imports in a file, resolves wrappers from those imported modules, and passes aParseContext(containingwrappersByLocalNameandnamespaceWrappers) intoparse(). Files with no directposthogliteral but with resolvable wrappers are still enriched.call-detector.tsconsumesParseContextto synthesizePostHogCallentries for bare and namespace wrapper calls, reusing the existing constant-map for identifier resolution.languages.tsfor JS/TS and Python.JSX-aware comment rendering
isInsideJsx()helper inast-helpers.tsthat walks the AST upward to detect JSX element ancestry.PostHogCall,CapturedEvent,FlagCheck,ListItem, andEnrichedListItemgain aninJsxfield.comment-formatter.tsnow emits{/* [PostHog] ... */}for JSX-context items and handles mixed JSX/JS items on the same line by inserting a leading JSX-style block comment instead of an inline suffix.Other fixes
FlagEvaluationStatsnow includeswindowDaysso the comment formatter can render the actual query window instead of a hardcoded7d.evaluationStatsErrorand rendered as"eval stats unavailable"rather than silently omitting the stat.parse-result.ts(with trailing-slash normalization) and passed viaflagUrlsonEnrichmentContext, removing thehost/projectIdfields from that interface.FunctionInfogainsbodyEndLine.ParserManageruses a stable string key for query cache entries instead oflang.toString().