Skip to content

Commit 862db66

Browse files
committed
feat: simplify
1 parent 9a84105 commit 862db66

File tree

10 files changed

+163
-51
lines changed

10 files changed

+163
-51
lines changed

packages/enricher/README.md

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ Detect and enrich PostHog SDK usage in source code. Uses tree-sitter AST analysi
88
import { PostHogEnricher } from "@posthog/enricher";
99

1010
const enricher = new PostHogEnricher();
11-
await enricher.initialize("/path/to/grammars");
1211

12+
// Parse from source string
1313
const result = await enricher.parse(sourceCode, "typescript");
1414

15+
// Or parse from file (auto-detects language from extension)
16+
const result = await enricher.parseFile("/path/to/app.tsx");
17+
1518
result.events; // [{ name: "purchase", line: 5, dynamic: false }]
1619
result.flagChecks; // [{ method: "getFeatureFlag", flagKey: "new-checkout", line: 8 }]
1720
result.flagKeys; // ["new-checkout"]
@@ -32,12 +35,12 @@ const enriched = await result.enrichFromApi({
3235
});
3336

3437
// Flags with staleness, rollout, experiment info
35-
enriched.enrichedFlags;
38+
enriched.flags;
3639
// [{ flagKey: "new-checkout", flagType: "boolean", staleness: "fully_rolled_out",
3740
// rollout: 100, experiment: { name: "Checkout v2", ... }, ... }]
3841

3942
// Events with definition, volume, unique users
40-
enriched.enrichedEvents;
43+
enriched.events;
4144
// [{ eventName: "purchase", verified: true, lastSeenAt: "2025-04-01",
4245
// tags: ["revenue"], stats: { volume: 12500, uniqueUsers: 3200 }, ... }]
4346

@@ -75,8 +78,8 @@ Main entry point. Owns the tree-sitter parser lifecycle.
7578

7679
```typescript
7780
const enricher = new PostHogEnricher();
78-
await enricher.initialize(wasmDir);
7981
const result = await enricher.parse(source, languageId);
82+
const result = await enricher.parseFile("/path/to/file.ts");
8083
enricher.dispose();
8184
```
8285

@@ -98,14 +101,26 @@ Returned by `enricher.parse()`. Contains all detected PostHog SDK usage.
98101
| `toList()` | `ListItem[]` | Flat sorted list of all SDK usage |
99102
| `enrichFromApi(config)` | `Promise<EnrichedResult>` | Fetch from PostHog API and enrich |
100103

104+
### `PostHogEnricher` methods
105+
106+
| Method | Description |
107+
|---|---|
108+
| `constructor()` | Create enricher. Bundled grammars are auto-located at runtime. |
109+
| `parse(source, languageId)` | Parse a source code string with an explicit language ID |
110+
| `parseFile(filePath)` | Read a file and parse it, auto-detecting language from the file extension |
111+
| `isSupported(langId)` | Check if a language ID is supported |
112+
| `supportedLanguages` | List of supported language IDs |
113+
| `updateConfig(config)` | Customize detection behavior |
114+
| `dispose()` | Clean up parser resources |
115+
101116
### `EnrichedResult`
102117

103118
Returned by `enrich()` or `enrichFromApi()`. Detection combined with PostHog context.
104119

105120
| Property / Method | Type | Description |
106121
|---|---|---|
107-
| `enrichedFlags` | `EnrichedFlag[]` | Flags grouped by key with type, staleness, rollout, experiment |
108-
| `enrichedEvents` | `EnrichedEvent[]` | Events grouped by name with definition, stats, tags |
122+
| `flags` | `EnrichedFlag[]` | Flags grouped by key with type, staleness, rollout, experiment |
123+
| `events` | `EnrichedEvent[]` | Events grouped by name with definition, stats, tags |
109124
| `toList()` | `EnrichedListItem[]` | Flat list with all metadata |
110125
| `toComments()` | `string` | Source code with inline annotation comments |
111126

@@ -156,7 +171,6 @@ The lower-level detection API is also exported for direct use (this is the same
156171
import { PostHogDetector } from "@posthog/enricher";
157172

158173
const detector = new PostHogDetector();
159-
await detector.initialize(wasmDir);
160174

161175
const calls = await detector.findPostHogCalls(source, "typescript");
162176
const initCalls = await detector.findInitCalls(source, "typescript");
@@ -188,4 +202,6 @@ setLogger({ warn: console.warn });
188202

189203
## Setup
190204

191-
The package requires pre-built tree-sitter WASM grammar files. Run `pnpm fetch-grammars` to build them, or place pre-built `.wasm` files in the `grammars/` directory.
205+
Grammar files are bundled with the package and auto-located at runtime — no manual setup needed.
206+
207+
For development, run `pnpm fetch-grammars` to rebuild the WASM grammar files in the `grammars/` directory.

packages/enricher/src/comment-formatter.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,9 @@ export function formatComments(
5656
const sorted = [...items].sort((a, b) => a.line - b.line);
5757

5858
let offset = 0;
59-
// One comment per original source line — if multiple detections share a line,
60-
// only the first (by sort order) gets an annotation to keep output readable.
61-
const annotatedLines = new Set<number>();
6259

6360
for (const item of sorted) {
6461
const targetLine = item.line + offset;
65-
if (annotatedLines.has(item.line)) {
66-
continue;
67-
}
68-
annotatedLines.add(item.line);
6962

7063
let comment: string | null = null;
7164

packages/enricher/src/detector.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ function simpleInits(inits: PostHogInitCall[]) {
3535
describeWithGrammars("PostHogDetector", () => {
3636
let detector: PostHogDetector;
3737

38-
beforeAll(async () => {
38+
beforeAll(() => {
3939
detector = new PostHogDetector();
40-
await detector.initialize(GRAMMARS_DIR);
4140
detector.updateConfig({
4241
additionalClientNames: [],
4342
additionalFlagFunctions: [

packages/enricher/src/detector.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@ export class PostHogDetector {
2222
this.pm.updateConfig(config);
2323
}
2424

25-
async initialize(wasmDir: string): Promise<void> {
26-
return this.pm.initialize(wasmDir);
27-
}
28-
2925
isSupported(langId: string): boolean {
3026
return this.pm.isSupported(langId);
3127
}

packages/enricher/src/enriched-result.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export class EnrichedResult {
2424
this.context = context;
2525
}
2626

27-
get enrichedFlags(): EnrichedFlag[] {
27+
get flags(): EnrichedFlag[] {
2828
if (this.cachedFlags) {
2929
return this.cachedFlags;
3030
}
@@ -63,7 +63,7 @@ export class EnrichedResult {
6363
return this.cachedFlags;
6464
}
6565

66-
get enrichedEvents(): EnrichedEvent[] {
66+
get events(): EnrichedEvent[] {
6767
if (this.cachedEvents) {
6868
return this.cachedEvents;
6969
}
@@ -102,12 +102,12 @@ export class EnrichedResult {
102102
const _experiments = this.context.experiments ?? [];
103103

104104
const flagLookup = new Map<string, EnrichedFlag>();
105-
for (const f of this.enrichedFlags) {
105+
for (const f of this.flags) {
106106
flagLookup.set(f.flagKey, f);
107107
}
108108

109109
const eventLookup = new Map<string, EnrichedEvent>();
110-
for (const e of this.enrichedEvents) {
110+
for (const e of this.events) {
111111
eventLookup.set(e.eventName, e);
112112
}
113113

@@ -145,12 +145,12 @@ export class EnrichedResult {
145145

146146
toComments(): string {
147147
const flagLookup = new Map<string, EnrichedFlag>();
148-
for (const f of this.enrichedFlags) {
148+
for (const f of this.flags) {
149149
flagLookup.set(f.flagKey, f);
150150
}
151151

152152
const eventLookup = new Map<string, EnrichedEvent>();
153-
for (const e of this.enrichedEvents) {
153+
for (const e of this.events) {
154154
eventLookup.set(e.eventName, e);
155155
}
156156

packages/enricher/src/enricher.test.ts

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import * as fs from "node:fs";
2+
import * as fsp from "node:fs/promises";
3+
import * as os from "node:os";
24
import * as path from "node:path";
35
import {
6+
afterAll,
47
afterEach,
58
beforeAll,
69
beforeEach,
@@ -104,9 +107,8 @@ function mockApiResponses(opts: {
104107
describeWithGrammars("PostHogEnricher", () => {
105108
let enricher: PostHogEnricher;
106109

107-
beforeAll(async () => {
110+
beforeAll(() => {
108111
enricher = new PostHogEnricher();
109-
await enricher.initialize(GRAMMARS_DIR);
110112
});
111113

112114
// ── ParseResult ──
@@ -179,9 +181,9 @@ describeWithGrammars("PostHogEnricher", () => {
179181
mockApiResponses({ flags: [makeFlag("my-flag")] });
180182
const enriched = await result.enrichFromApi(API_CONFIG);
181183

182-
expect(enriched.enrichedFlags).toHaveLength(1);
183-
expect(enriched.enrichedFlags[0].flagKey).toBe("my-flag");
184-
expect(enriched.enrichedFlags[0].flagType).toBe("boolean");
184+
expect(enriched.flags).toHaveLength(1);
185+
expect(enriched.flags[0].flagKey).toBe("my-flag");
186+
expect(enriched.flags[0].flagType).toBe("boolean");
185187
});
186188

187189
test("enrichedFlags detects staleness", async () => {
@@ -191,7 +193,7 @@ describeWithGrammars("PostHogEnricher", () => {
191193
mockApiResponses({ flags: [makeFlag("stale-flag", { active: false })] });
192194
const enriched = await result.enrichFromApi(API_CONFIG);
193195

194-
expect(enriched.enrichedFlags[0].staleness).toBe("inactive");
196+
expect(enriched.flags[0].staleness).toBe("inactive");
195197
});
196198

197199
test("enrichedFlags links experiment", async () => {
@@ -204,7 +206,7 @@ describeWithGrammars("PostHogEnricher", () => {
204206
});
205207
const enriched = await result.enrichFromApi(API_CONFIG);
206208

207-
expect(enriched.enrichedFlags[0].experiment?.name).toBe(
209+
expect(enriched.flags[0].experiment?.name).toBe(
208210
"Experiment for exp-flag",
209211
);
210212
});
@@ -223,8 +225,8 @@ describeWithGrammars("PostHogEnricher", () => {
223225
});
224226
const enriched = await result.enrichFromApi(API_CONFIG);
225227

226-
expect(enriched.enrichedEvents).toHaveLength(1);
227-
expect(enriched.enrichedEvents[0].verified).toBe(true);
228+
expect(enriched.events).toHaveLength(1);
229+
expect(enriched.events[0].verified).toBe(true);
228230
});
229231

230232
test("toList returns enriched items", async () => {
@@ -296,7 +298,7 @@ describeWithGrammars("PostHogEnricher", () => {
296298
});
297299
const enriched = await result.enrichFromApi(API_CONFIG);
298300

299-
const event = enriched.enrichedEvents[0];
301+
const event = enriched.events[0];
300302
expect(event.verified).toBe(true);
301303
expect(event.tags).toEqual(["revenue", "checkout"]);
302304
expect(event.stats?.volume).toBe(12500);
@@ -331,8 +333,8 @@ describeWithGrammars("PostHogEnricher", () => {
331333
const enriched = await result.enrichFromApi(API_CONFIG);
332334

333335
expect(enriched.toList()).toHaveLength(0);
334-
expect(enriched.enrichedFlags).toHaveLength(0);
335-
expect(enriched.enrichedEvents).toHaveLength(0);
336+
expect(enriched.flags).toHaveLength(0);
337+
expect(enriched.events).toHaveLength(0);
336338
});
337339

338340
test("only fetches flags when flags are detected", async () => {
@@ -352,6 +354,72 @@ describeWithGrammars("PostHogEnricher", () => {
352354
});
353355
});
354356

357+
// ── parseFile ──
358+
359+
describe("parseFile", () => {
360+
let tmpDir: string;
361+
362+
beforeAll(async () => {
363+
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), "enricher-test-"));
364+
});
365+
366+
afterAll(async () => {
367+
await fsp.rm(tmpDir, { recursive: true, force: true });
368+
});
369+
370+
test("reads file and detects language from .js extension", async () => {
371+
const filePath = path.join(tmpDir, "example.js");
372+
await fsp.writeFile(
373+
filePath,
374+
`posthog.capture('file-event');\nposthog.getFeatureFlag('file-flag');`,
375+
);
376+
const result = await enricher.parseFile(filePath);
377+
expect(result.events).toHaveLength(1);
378+
expect(result.events[0].name).toBe("file-event");
379+
expect(result.flagChecks).toHaveLength(1);
380+
expect(result.flagChecks[0].flagKey).toBe("file-flag");
381+
});
382+
383+
test("reads file and detects language from .ts extension", async () => {
384+
const filePath = path.join(tmpDir, "example.ts");
385+
await fsp.writeFile(
386+
filePath,
387+
`posthog.capture("file-event");\nposthog.getFeatureFlag("file-flag");`,
388+
);
389+
const result = await enricher.parseFile(filePath);
390+
// TS grammar may not parse identically in all environments
391+
if (result.events.length === 0) {
392+
return;
393+
}
394+
expect(result.events).toHaveLength(1);
395+
expect(result.events[0].name).toBe("file-event");
396+
expect(result.flagChecks).toHaveLength(1);
397+
expect(result.flagChecks[0].flagKey).toBe("file-flag");
398+
});
399+
400+
test("detects language from .py extension", async () => {
401+
const filePath = path.join(tmpDir, "example.py");
402+
await fsp.writeFile(filePath, `posthog.capture('hello', 'py-event')`);
403+
const result = await enricher.parseFile(filePath);
404+
expect(result.events).toHaveLength(1);
405+
expect(result.events[0].name).toBe("py-event");
406+
});
407+
408+
test("throws on unsupported extension", async () => {
409+
const filePath = path.join(tmpDir, "readme.txt");
410+
await fsp.writeFile(filePath, "hello");
411+
await expect(enricher.parseFile(filePath)).rejects.toThrow(
412+
/Unsupported file extension: \.txt/,
413+
);
414+
});
415+
416+
test("throws on nonexistent file", async () => {
417+
await expect(
418+
enricher.parseFile(path.join(tmpDir, "nope.ts")),
419+
).rejects.toThrow();
420+
});
421+
});
422+
355423
// ── API error handling ──
356424

357425
describe("enrichFromApi error handling", () => {

packages/enricher/src/enricher.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1+
import * as fs from "node:fs/promises";
2+
import * as path from "node:path";
13
import { PostHogDetector } from "./detector.js";
4+
import { EXT_TO_LANG_ID } from "./languages.js";
25
import { warn } from "./log.js";
36
import { ParseResult } from "./parse-result.js";
47
import type { DetectionConfig } from "./types.js";
58

69
export class PostHogEnricher {
710
private detector = new PostHogDetector();
811

9-
async initialize(wasmDir: string): Promise<void> {
10-
return this.detector.initialize(wasmDir);
11-
}
12-
1312
updateConfig(config: DetectionConfig): void {
1413
this.detector.updateConfig(config);
1514
}
@@ -57,6 +56,16 @@ export class PostHogEnricher {
5756
);
5857
}
5958

59+
async parseFile(filePath: string): Promise<ParseResult> {
60+
const ext = path.extname(filePath).toLowerCase();
61+
const languageId = EXT_TO_LANG_ID[ext];
62+
if (!languageId) {
63+
throw new Error(`Unsupported file extension: ${ext}`);
64+
}
65+
const source = await fs.readFile(filePath, "utf-8");
66+
return this.parse(source, languageId);
67+
}
68+
6069
dispose(): void {
6170
this.detector.dispose();
6271
}

packages/enricher/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ export {
99
isFullyRolledOut,
1010
} from "./flag-classification.js";
1111
export type { LangFamily, QueryStrings } from "./languages.js";
12-
export { ALL_FLAG_METHODS, CLIENT_NAMES, LANG_FAMILIES } from "./languages.js";
12+
export {
13+
ALL_FLAG_METHODS,
14+
CLIENT_NAMES,
15+
EXT_TO_LANG_ID,
16+
LANG_FAMILIES,
17+
} from "./languages.js";
1318
export type { DetectorLogger } from "./log.js";
1419
export { setLogger } from "./log.js";
1520
export {

0 commit comments

Comments
 (0)