Skip to content

feat: enrich wrapped functions and support jsx#1837

Merged
k11kirky merged 1 commit intomainfrom
04-22-feat_enrich_wrapped_functions_and_support_jsx
Apr 23, 2026
Merged

feat: enrich wrapped functions and support jsx#1837
k11kirky merged 1 commit intomainfrom
04-22-feat_enrich_wrapped_functions_and_support_jsx

Conversation

@k11kirky
Copy link
Copy Markdown
Contributor

@k11kirky k11kirky commented Apr 22, 2026

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 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).
  • PostHogEnricher gains getWrappersForFile(absPath) (with mtime-based LRU cache) and findImportsInSource(source, langId, callerAbsPath).
  • 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().

Copy link
Copy Markdown
Contributor Author

k11kirky commented Apr 22, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@k11kirky k11kirky marked this pull request as ready for review April 22, 2026 14:20
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Comments Outside Diff (4)

  1. packages/enricher/src/enricher.ts, line 1215-1216 (link)

    P1 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.

      try {
        const wrappers = await this.detector.findWrappers(source, languageId);
        return this.setWrapperCache(absPath, mtimeMs, wrappers);
      } catch {
        return this.setWrapperCache(absPath, mtimeMs, []);
      }
    Prompt To Fix 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.
    
    ```typescript
      try {
        const wrappers = await this.detector.findWrappers(source, languageId);
        return this.setWrapperCache(absPath, mtimeMs, wrappers);
      } catch {
        return this.setWrapperCache(absPath, mtimeMs, []);
      }
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. packages/enricher/src/import-resolver.ts, line 1482-1488 (link)

    P2 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.
  3. packages/enricher/src/enricher.ts, line 1241-1248 (link)

    P2 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:

    private setWrapperCache(
      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 });
      return wrappers;
    }
    Prompt To Fix With AI
    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:
    
    ```typescript
    private setWrapperCache(
      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 });
      return wrappers;
    }
    ```
    
    How can I resolve this? If you propose a fix, please make it concise.
  4. packages/enricher/src/wrapper-integration.test.ts, line 2682-2722 (link)

    P2 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.

```typescript
  try {
    const wrappers = await this.detector.findWrappers(source, languageId);
    return this.setWrapperCache(absPath, mtimeMs, wrappers);
  } catch {
    return this.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:

```typescript
private setWrapperCache(
  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 });
  return wrappers;
}
```

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.

Reviews (1): Last reviewed commit: "feat: enrich wrapped functions and suppo..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@jonathanlab jonathanlab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢 yes

Copy link
Copy Markdown
Contributor Author

k11kirky commented Apr 23, 2026

Merge activity

  • Apr 23, 10:03 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 23, 10:04 AM UTC: Graphite rebased this pull request as part of a merge.
  • Apr 23, 10:15 AM UTC: @k11kirky merged this pull request with Graphite.

@k11kirky k11kirky changed the base branch from 04-21-feat_improve_flag_enricher to graphite-base/1837 April 23, 2026 10:03
@k11kirky k11kirky changed the base branch from graphite-base/1837 to main April 23, 2026 10:03
@k11kirky k11kirky force-pushed the 04-22-feat_enrich_wrapped_functions_and_support_jsx branch from e3824f6 to 0868f9f Compare April 23, 2026 10:04
@k11kirky k11kirky merged commit b97f2ec into main Apr 23, 2026
16 checks passed
@k11kirky k11kirky deleted the 04-22-feat_enrich_wrapped_functions_and_support_jsx branch April 23, 2026 10:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants