Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/enricher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./classification": {
"types": "./dist/flag-classification.d.ts",
"import": "./dist/flag-classification.js"
},
"./stale-flags": {
"types": "./dist/stale-flags.d.ts",
"import": "./dist/stale-flags.js"
},
"./types": {
"types": "./dist/types.d.ts",
"import": "./dist/types.js"
}
},
"scripts": {
Expand All @@ -28,7 +40,6 @@
},
"files": [
"dist/**/*",
"src/**/*",
"grammars/**/*"
]
}
164 changes: 164 additions & 0 deletions packages/enricher/src/detector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,4 +447,168 @@ describeWithGrammars("PostHogDetector", () => {
]);
});
});

// ═══════════════════════════════════════════════════
// Python — additional findPostHogCalls / findInitCalls
// ═══════════════════════════════════════════════════

describe("Python — findPostHogCalls (capture)", () => {
test("detects capture with positional event arg", async () => {
const code = `posthog.capture('user_id', 'purchase')`;
const calls = await detector.findPostHogCalls(code, "python");
const capture = calls.find(
(c) => c.method === "capture" && c.key === "purchase",
);
expect(capture).toBeDefined();
});

test("detects flag method get_feature_flag", async () => {
const code = `posthog.get_feature_flag('my-flag')`;
const calls = await detector.findPostHogCalls(code, "python");
expect(simpleCalls(calls)).toEqual([
{ line: 0, method: "get_feature_flag", key: "my-flag" },
]);
});
});

describe("Python — findInitCalls", () => {
test("detects positional constructor Posthog('phc_token')", async () => {
const code = `Posthog('phc_token')`;
const inits = await detector.findInitCalls(code, "python");
expect(inits).toHaveLength(1);
expect(inits[0].token).toBe("phc_token");
});

test("detects keyword constructor with api_key and host", async () => {
const code = `Posthog(api_key='phc_token', host='https://app.posthog.com')`;
const inits = await detector.findInitCalls(code, "python");
expect(inits).toHaveLength(1);
expect(inits[0].token).toBe("phc_token");
expect(inits[0].apiHost).toBe("https://app.posthog.com");
});
});

// ═══════════════════════════════════════════════════
// Go — additional findPostHogCalls / findInitCalls
// ═══════════════════════════════════════════════════

describe("Go — findPostHogCalls (capture & flags)", () => {
test("detects struct-based Enqueue capture", async () => {
const code = [
`package main`,
``,
`func main() {`,
` client.Enqueue(posthog.Capture{Event: "purchase"})`,
`}`,
].join("\n");
const calls = await detector.findPostHogCalls(code, "go");
const capture = calls.find(
(c) => c.method === "capture" && c.key === "purchase",
);
expect(capture).toBeDefined();
});

test("detects flag method GetFeatureFlag", async () => {
const code = [
`package main`,
``,
`func main() {`,
` client.GetFeatureFlag(posthog.FeatureFlagPayload{Key: "my-flag"})`,
`}`,
].join("\n");
const calls = await detector.findPostHogCalls(code, "go");
const flag = calls.find(
(c) => c.method === "GetFeatureFlag" && c.key === "my-flag",
);
expect(flag).toBeDefined();
});
});

describe("Go — findInitCalls", () => {
test("detects posthog.New constructor", async () => {
const code = [
`package main`,
``,
`func main() {`,
` client := posthog.New("phc_token")`,
`}`,
].join("\n");
const inits = await detector.findInitCalls(code, "go");
expect(inits).toHaveLength(1);
expect(inits[0].token).toBe("phc_token");
});

test("detects posthog.NewWithConfig constructor", async () => {
const code = [
`package main`,
``,
`func main() {`,
` client, _ := posthog.NewWithConfig("phc_token", posthog.Config{Endpoint: "https://app.posthog.com"})`,
`}`,
].join("\n");
const inits = await detector.findInitCalls(code, "go");
expect(inits).toHaveLength(1);
expect(inits[0].token).toBe("phc_token");
expect(inits[0].apiHost).toBe("https://app.posthog.com");
});
});

// ═══════════════════════════════════════════════════
// Ruby — additional findPostHogCalls / findInitCalls
// ═══════════════════════════════════════════════════

describe("Ruby — findPostHogCalls (capture & flags)", () => {
test("detects capture with keyword args", async () => {
const code = `client.capture(distinct_id: 'user', event: 'purchase')`;
const calls = await detector.findPostHogCalls(code, "ruby");
const capture = calls.find(
(c) => c.method === "capture" && c.key === "purchase",
);
expect(capture).toBeDefined();
});

test("detects flag method get_feature_flag", async () => {
const code = `client.get_feature_flag('my-flag')`;
const calls = await detector.findPostHogCalls(code, "ruby");
const flag = calls.find(
(c) => c.method === "get_feature_flag" && c.key === "my-flag",
);
expect(flag).toBeDefined();
});
});

describe("Ruby — findInitCalls", () => {
test("detects PostHog::Client.new constructor", async () => {
const code = `client = PostHog::Client.new(api_key: 'phc_token')`;
const inits = await detector.findInitCalls(code, "ruby");
expect(inits).toHaveLength(1);
expect(inits[0].token).toBe("phc_token");
});
});

// ═══════════════════════════════════════════════════
// Negative / edge cases
// ═══════════════════════════════════════════════════

describe("Negative / edge cases", () => {
test("unsupported language returns empty arrays", async () => {
const code = `posthog.capture('event')`;
const calls = await detector.findPostHogCalls(code, "haskell");
const inits = await detector.findInitCalls(code, "haskell");
expect(calls).toEqual([]);
expect(inits).toEqual([]);
});

test("non-PostHog client names are ignored", async () => {
const code = `other.capture('event')`;

const jsCalls = await detector.findPostHogCalls(code, "javascript");
const pyCalls = await detector.findPostHogCalls(code, "python");
const rbCalls = await detector.findPostHogCalls(code, "ruby");

expect(jsCalls).toEqual([]);
expect(pyCalls).toEqual([]);
expect(rbCalls).toEqual([]);
});
});
});
11 changes: 11 additions & 0 deletions packages/enricher/src/parser-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class ParserManager {
private parser: Parser | null = null;
private languages = new Map<string, Parser.Language>();
private queryCache = new Map<string, Parser.Query>();
private maxCacheSize = 256;
private initPromise: Promise<void> | null = null;
private wasmDir = "";
config: DetectionConfig = DEFAULT_CONFIG;
Expand Down Expand Up @@ -91,11 +92,21 @@ export class ParserManager {
const cacheKey = `${lang.toString()}:${queryStr}`;
let query = this.queryCache.get(cacheKey);
if (query) {
// LRU: move to end by deleting and re-inserting
this.queryCache.delete(cacheKey);
this.queryCache.set(cacheKey, query);
return query;
}

try {
query = lang.query(queryStr);
// Evict oldest entry if at capacity
if (this.queryCache.size >= this.maxCacheSize) {
const oldest = this.queryCache.keys().next().value;
if (oldest !== undefined) {
this.queryCache.delete(oldest);
}
}
this.queryCache.set(cacheKey, query);
return query;
} catch (err) {
Expand Down
7 changes: 6 additions & 1 deletion packages/enricher/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
entry: [
"src/index.ts",
"src/flag-classification.ts",
"src/stale-flags.ts",
"src/types.ts",
],
format: ["esm"],
dts: true,
sourcemap: true,
Expand Down
Loading