-
Notifications
You must be signed in to change notification settings - Fork 11
feat: improve flag enricher #1786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,6 +82,7 @@ function mockApiResponses(opts: { | |
| experiments?: Experiment[]; | ||
| eventDefs?: EventDefinition[]; | ||
| eventStats?: [string, number, number, string][]; | ||
| flagEvalStats?: [string, number, number][]; | ||
| }): void { | ||
| const mockFetch = vi.fn(async (url: string, init?: RequestInit) => { | ||
| const urlStr = typeof url === "string" ? url : String(url); | ||
|
|
@@ -96,6 +97,15 @@ function mockApiResponses(opts: { | |
| return Response.json({ results: opts.eventDefs ?? [] }); | ||
| } | ||
| if (urlStr.includes("/query/") && init?.method === "POST") { | ||
| const body = | ||
| typeof init.body === "string" | ||
| ? init.body | ||
| : init.body instanceof Uint8Array | ||
| ? new TextDecoder().decode(init.body) | ||
| : ""; | ||
| if (body.includes("$feature_flag_called")) { | ||
| return Response.json({ results: opts.flagEvalStats ?? [] }); | ||
| } | ||
| return Response.json({ results: opts.eventStats ?? [] }); | ||
| } | ||
| return Response.json({}); | ||
|
|
@@ -383,6 +393,109 @@ describeWithGrammars("PostHogEnricher", () => { | |
| expect(annotated).toContain("1,200 users"); | ||
| }); | ||
|
|
||
| test("enrichedFlags includes url and evaluation stats", async () => { | ||
| const code = `posthog.getFeatureFlag('my-flag');`; | ||
| const result = await enricher.parse(code, "javascript"); | ||
|
|
||
| mockApiResponses({ | ||
| flags: [makeFlag("my-flag", { id: 42 })], | ||
| flagEvalStats: [["my-flag", 1240, 230]], | ||
| }); | ||
| const enriched = await result.enrichFromApi(API_CONFIG); | ||
|
|
||
| expect(enriched.flags[0].url).toBe( | ||
| "https://test.posthog.com/project/1/feature_flags/42", | ||
| ); | ||
| expect(enriched.flags[0].evaluationStats).toEqual({ | ||
| evaluations: 1240, | ||
| uniqueUsers: 230, | ||
| }); | ||
| }); | ||
|
|
||
| test("toComments renders rich flag line with url, active, rollout, evals", async () => { | ||
| const code = `posthog.getFeatureFlag('my-flag');`; | ||
| const result = await enricher.parse(code, "javascript"); | ||
|
|
||
| mockApiResponses({ | ||
| flags: [ | ||
| makeFlag("my-flag", { | ||
| id: 42, | ||
| filters: { groups: [{ rollout_percentage: 60, properties: [] }] }, | ||
| }), | ||
| ], | ||
| flagEvalStats: [["my-flag", 1240, 230]], | ||
| }); | ||
| const enriched = await result.enrichFromApi(API_CONFIG); | ||
|
|
||
| const annotated = enriched.toComments(); | ||
| expect(annotated).toContain(`Flag: "my-flag"`); | ||
| expect(annotated).toContain("active"); | ||
| expect(annotated).toContain("60% rolled out"); | ||
| expect(annotated).toContain("1,240 evals / 230 users (7d)"); | ||
| expect(annotated).toContain( | ||
| "https://test.posthog.com/project/1/feature_flags/42", | ||
| ); | ||
| }); | ||
|
|
||
| test("toComments marks inactive flags explicitly", async () => { | ||
| const code = `posthog.getFeatureFlag('off-flag');`; | ||
| const result = await enricher.parse(code, "javascript"); | ||
|
|
||
| mockApiResponses({ flags: [makeFlag("off-flag", { active: false })] }); | ||
| const enriched = await result.enrichFromApi(API_CONFIG); | ||
|
|
||
| const annotated = enriched.toComments(); | ||
| expect(annotated).toContain("inactive"); | ||
| expect(annotated).toContain("STALE (inactive)"); | ||
| }); | ||
|
|
||
| test("toComments handles flag not in PostHog", async () => { | ||
| const code = `posthog.getFeatureFlag('ghost-flag');`; | ||
| const result = await enricher.parse(code, "javascript"); | ||
|
|
||
| mockApiResponses({ flags: [] }); | ||
| const enriched = await result.enrichFromApi(API_CONFIG); | ||
|
|
||
| const annotated = enriched.toComments(); | ||
| expect(annotated).toContain(`Flag: "ghost-flag" \u2014 not in PostHog`); | ||
| }); | ||
|
|
||
| test("toComments omits evaluation segment when stats missing", async () => { | ||
| const code = `posthog.getFeatureFlag('quiet-flag');`; | ||
| const result = await enricher.parse(code, "javascript"); | ||
|
|
||
| mockApiResponses({ flags: [makeFlag("quiet-flag", { id: 7 })] }); | ||
| const enriched = await result.enrichFromApi(API_CONFIG); | ||
|
|
||
| const annotated = enriched.toComments(); | ||
| expect(annotated).toContain(`Flag: "quiet-flag"`); | ||
| expect(annotated).not.toContain("evals /"); | ||
| expect(annotated).toContain( | ||
| "https://test.posthog.com/project/1/feature_flags/7", | ||
| ); | ||
| }); | ||
|
|
||
| test("getFlagEvaluationStats is called with detected flag keys", async () => { | ||
| const code = `posthog.getFeatureFlag('tracked-flag');`; | ||
| const result = await enricher.parse(code, "javascript"); | ||
|
|
||
| mockApiResponses({ | ||
| flags: [makeFlag("tracked-flag")], | ||
| flagEvalStats: [["tracked-flag", 10, 5]], | ||
| }); | ||
| await result.enrichFromApi(API_CONFIG); | ||
|
|
||
| const calls = vi.mocked(fetch).mock.calls; | ||
| const queryPost = calls.find( | ||
| ([url, init]) => | ||
| String(url).includes("/query/") && init?.method === "POST", | ||
| ); | ||
| expect(queryPost).toBeDefined(); | ||
| const body = String(queryPost?.[1]?.body ?? ""); | ||
| expect(body).toContain("$feature_flag_called"); | ||
| expect(body).toContain("tracked-flag"); | ||
| }); | ||
|
|
||
| test("enrichFromApi with no detected usage returns empty enrichment", async () => { | ||
|
Comment on lines
+402
to
499
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The five new test.each([
{
name: "rich line with url, active, rollout, evals",
flagOpts: { id: 42, filters: { groups: [{ rollout_percentage: 60, properties: [] }] } },
flagKey: "my-flag",
evalStats: [["my-flag", 1240, 230]] as [string, number, number][],
contains: ["active", "60% rolled out", "1,240 evals / 230 users (7d)", "feature_flags/42"],
notContains: [],
},
// … inactive, ghost, quiet-flag variants …
])("toComments $name", async ({ flagKey, flagOpts, evalStats, contains, notContains }) => {
const result = await enricher.parse(`posthog.getFeatureFlag('${flagKey}');`, "javascript");
mockApiResponses({ flags: [makeFlag(flagKey, flagOpts)], flagEvalStats: evalStats });
const annotated = (await result.enrichFromApi(API_CONFIG)).toComments();
contains.forEach((s) => expect(annotated).toContain(s));
notContains.forEach((s) => expect(annotated).not.toContain(s));
});This reduces repetition without losing coverage. Context Used: Do not attempt to comment on incorrect alphabetica... (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: packages/enricher/src/enricher.test.ts
Line: 402-499
Comment:
**Prefer parameterised tests**
The five new `toComments` tests share the same three-step skeleton — parse a snippet, call `mockApiResponses`, assert on `toComments()` output — and differ only in the flag configuration and expected substrings. Per the team's convention they should be a single `test.each` table, e.g.:
```ts
test.each([
{
name: "rich line with url, active, rollout, evals",
flagOpts: { id: 42, filters: { groups: [{ rollout_percentage: 60, properties: [] }] } },
flagKey: "my-flag",
evalStats: [["my-flag", 1240, 230]] as [string, number, number][],
contains: ["active", "60% rolled out", "1,240 evals / 230 users (7d)", "feature_flags/42"],
notContains: [],
},
// … inactive, ghost, quiet-flag variants …
])("toComments $name", async ({ flagKey, flagOpts, evalStats, contains, notContains }) => {
const result = await enricher.parse(`posthog.getFeatureFlag('${flagKey}');`, "javascript");
mockApiResponses({ flags: [makeFlag(flagKey, flagOpts)], flagEvalStats: evalStats });
const annotated = (await result.enrichFromApi(API_CONFIG)).toComments();
contains.forEach((s) => expect(annotated).toContain(s));
notContains.forEach((s) => expect(annotated).not.toContain(s));
});
```
This reduces repetition without losing coverage.
**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
| const code = `const x = 1;`; | ||
| const result = await enricher.parse(code, "javascript"); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a flag has
active: false, the comment will contain both the explicit"inactive"segment (line 21) and"STALE (inactive)"from the staleness classifier — confirmed by the test that asserts both are present. The staleness segment already communicates the reason the flag is inactive, so displaying"inactive"as a separate part is redundant and violates the OnceAndOnlyOnce simplicity rule.Consider either suppressing the
active/inactivelabel whenflag.staleness === "inactive", or only showing the active/inactive label and dropping it from the staleness display.Context Used: Do not attempt to comment on incorrect alphabetica... (source)
Prompt To Fix With AI