Skip to content

fix(abtest): resolve cross-PR review findings - #51

Merged
imjlk merged 7 commits into
mainfrom
fix/abtest-review-followup
Jul 25, 2026
Merged

fix(abtest): resolve cross-PR review findings#51
imjlk merged 7 commits into
mainfrom
fix/abtest-review-followup

Conversation

@imjlk

@imjlk imjlk commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Addresses unresolved codex/CodeRabbit findings across Change Sets A-E:

High

  • Hypothesis deep clone: lockHypothesis and the pre-locked branch in createTest now return structuredClone results so the stored hypothesis is fully detached from the caller's config object. Post-lock mutation of nested objects can no longer silently invalidate the checksum.
  • Report revenue gap: ExperimentReport variants now carry optional revenue and revenuePerRecipient. The Markdown table conditionally includes revenue columns when data is present.

Low

  • Stratification missing-key guard: groupOrder keys missing from groupExactCounts now throw instead of silently defaulting to zero.
  • Dead code: removed duplicate experimentFamilyKey empty check.
  • Table dedup: unified the revenue/non-revenue Markdown table branches.

Gates

308 abtest tests pass, bun run check (201 architecture paths), build succeeds.

@codex review

Summary by CodeRabbit

  • New Features
    • Added revenue and revenue-per-recipient columns to experiment reports when available.
  • Bug Fixes
    • Prevented locked hypothesis data from being changed unintentionally after validation.
    • Improved validation for experiment family keys.
    • Added clear errors when stratification data is missing required groups.
    • Removed redundant validation behavior.

imjlk added 2 commits July 26, 2026 03:58
Addresses unresolved codex/CodeRabbit findings across Change Sets A-E:

High:
- hypothesis.ts + abtest-service.ts: lockHypothesis and the pre-locked
  branch now return deep clones (structuredClone) so the stored
  hypothesis is fully detached from the caller's config object.
  Post-lock mutation of the caller's nested objects can no longer
  silently invalidate the stored checksum.
- report.ts: ExperimentReport variants now carry optional revenue and
  revenuePerRecipient fields. The Markdown table conditionally includes
  revenue columns when any variant has revenue data, so a
  revenue_per_recipient primary metric is reportable end-to-end.

Low:
- stratification.ts: groupOrder keys missing from groupExactCounts now
  throw instead of silently defaulting to zero.
- hypothesis.ts: removed dead duplicate experimentFamilyKey empty check.
OpenCodeReview suggestion: unify the revenue/non-revenue table branches
into a single headers+cells loop.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@imjlk, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f8b589fa-51b6-4e3f-8c45-dee425295bbb

📥 Commits

Reviewing files that changed from the base of the PR and between 8be2fd0 and 76d61b1.

📒 Files selected for processing (3)
  • README.md
  • README_ko.md
  • packages/abtest/src/report.ts
📝 Walkthrough

Walkthrough

The PR deep-clones locked hypotheses, tightens experiment family key validation, adds optional revenue metrics to experiment reports, and rejects missing stratification group counts.

Changes

AB test follow-ups

Layer / File(s) Summary
Hypothesis integrity and release metadata
.sampo/changesets/abtest-review-followup.md, packages/abtest/src/hypothesis.ts, packages/abtest/src/abtest-service.ts
Locked hypotheses are deep-cloned before storage and during locking; experiment family keys must be non-empty strings; the changeset records the fixes.
Revenue-aware experiment reports
packages/abtest/src/report.ts
Variant reports include optional revenue fields, calculate revenue per recipient when applicable, and conditionally render revenue columns and missing values.
Stratified quota group validation
packages/abtest/src/stratification.ts
Quota computation throws when a group in groupOrder has no corresponding exact count.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

A bunny checks the checksum tight,
Clones nested thoughts out of sight.
Revenue columns hop in line,
Missing groups now draw a sign.
Reports bloom bright in markdown light!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately reflects that this PR fixes review findings in the abtest package.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/abtest-review-followup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/abtest/src/abtest-service.ts`:
- Around line 209-211: The ab-test service exposes mutable internal test
objects, allowing callers to change stored hypotheses and invalidate checksums.
Update the storage and retrieval paths around abTest creation plus getTest,
getAllTests, and snapshotTests to deep-clone tests both when storing and before
returning them, preserving independent internal state at every service boundary;
add a regression test that mutates a returned hypothesis and verifies the stored
test remains unchanged.

In `@packages/abtest/src/stratification.ts`:
- Around line 222-223: Update the missing-group validation around
groupExactCounts in the stratification logic to first verify that groupKey is an
own property, rather than relying on groupExactCounts[groupKey] === undefined.
Preserve the existing missing-key handling, and only read the exact count after
the own-property check succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0823473c-886f-41c3-9fd4-d99b952ca2f5

📥 Commits

Reviewing files that changed from the base of the PR and between 4b79da7 and 8be2fd0.

📒 Files selected for processing (5)
  • .sampo/changesets/abtest-review-followup.md
  • packages/abtest/src/abtest-service.ts
  • packages/abtest/src/hypothesis.ts
  • packages/abtest/src/report.ts
  • packages/abtest/src/stratification.ts

Comment on lines +209 to +211
// Deep-clone so the stored test's hypothesis is
// detached from the caller's config object.
return structuredClone(config.hypothesis!);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Return defensive copies of stored tests.

This clone detaches config.hypothesis, but Lines 355-356 store and return the same mutable abTest; getTest, getAllTests, and snapshotTests also expose internal references. A caller can mutate the returned hypothesis and invalidate its checksum after creation. Return deep defensive copies, or encapsulate/freeze stored state, at every service boundary and add a regression test for this mutation path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/abtest/src/abtest-service.ts` around lines 209 - 211, The ab-test
service exposes mutable internal test objects, allowing callers to change stored
hypotheses and invalidate checksums. Update the storage and retrieval paths
around abTest creation plus getTest, getAllTests, and snapshotTests to
deep-clone tests both when storing and before returning them, preserving
independent internal state at every service boundary; add a regression test that
mutates a returned hypothesis and verifies the stored test remains unchanged.

Comment on lines +222 to +223
const exactCount = groupExactCounts[groupKey];
if (exactCount === undefined) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Check for an own property, not just an undefined lookup.

groupExactCounts[groupKey] traverses the prototype chain, while the earlier Object.entries(groupExactCounts) validation only considers own properties. An object with an inherited group key can therefore bypass this missing-key invariant. Use an own-property check before reading the count.

Proposed fix
 for (const [index, groupKey] of groupOrder.entries()) {
+	if (!Object.prototype.hasOwnProperty.call(groupExactCounts, groupKey)) {
+		throw new Error(
+			`Stratified quota invariant: group "${groupKey}" is in groupOrder but missing from groupExactCounts`,
+		);
+	}
 	const exactCount = groupExactCounts[groupKey];
-	if (exactCount === undefined) {
-		throw new Error(
-			`Stratified quota invariant: group "${groupKey}" is in groupOrder but missing from groupExactCounts`,
-		);
-	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const exactCount = groupExactCounts[groupKey];
if (exactCount === undefined) {
if (!Object.prototype.hasOwnProperty.call(groupExactCounts, groupKey)) {
throw new Error(
`Stratified quota invariant: group "${groupKey}" is in groupOrder but missing from groupExactCounts`,
);
}
const exactCount = groupExactCounts[groupKey];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/abtest/src/stratification.ts` around lines 222 - 223, Update the
missing-group validation around groupExactCounts in the stratification logic to
first verify that groupKey is an own property, rather than relying on
groupExactCounts[groupKey] === undefined. Preserve the existing missing-key
handling, and only read the exact count after the own-property check succeeds.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8be2fd0c28

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/abtest/src/report.ts Outdated
lines.push("");
lines.push("| Variant | Sample | Open Rate | Click Rate | Conversion Rate |");
lines.push("|---------|--------|-----------|------------|-----------------|");
const hasRevenue = report.variants.some((v) => v.revenue !== undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check both revenue fields before omitting columns

When an exported ExperimentReport is constructed with revenuePerRecipient but no total revenue—a shape explicitly permitted by the new interface—hasRevenue is false, so the Markdown report silently omits the available per-recipient metric even when primaryMetric is revenue_per_recipient. Include either optional revenue field in this presence check so direct callers of reportToMarkdown do not lose valid report data.

Useful? React with 👍 / 👎.

Comment on lines +49 to +50
revenue?: number;
revenuePerRecipient?: number;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve the currency alongside reported revenue

The new report fields carry monetary totals and per-recipient values without their currency, even though conversion events require an ISO 4217 currency and VariantConversionAggregate preserves it in packages/abtest/src/conversion-events.ts:28-41. Consequently, a report built from USD, KRW, or even different currencies across variants produces identical unlabeled numbers, making the new revenue columns ambiguous and potentially misleading; propagate the currency into TestResults/ExperimentReport and display it with these values.

Useful? React with 👍 / 👎.

Comment thread packages/abtest/src/report.ts Outdated
Comment on lines +276 to +277
if (hasRevenue) {
headers.push("Revenue", "Rev/Recipient");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Document the revenue report output in both user guides

This adds user-visible revenue columns to the Markdown experiment report, but neither README.md nor README_ko.md documents the new output; their hypothesis-report sections still describe only the pre-registration fields. Update both guides together so the documented report contract remains aligned with the released behavior.

AGENTS.md reference: AGENTS.md:L235-L237

Useful? React with 👍 / 👎.

Addresses @codex P1/P2 findings on PR #51:

- hasRevenue now checks both revenue AND revenuePerRecipient.
- ExperimentReport gains an optional revenueCurrency field (ISO 4217)
  so reported monetary values carry their currency.
@imjlk

imjlk commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Commit 174e52a: hasRevenue checks both fields, revenueCurrency added. @codex review

OpenCodeReview finding: revenueCurrency was declared but unused. Now
appends the currency code to the Revenue/Rev/Recipient column headers
(e.g. 'Revenue (USD)') when present.
@imjlk

imjlk commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Commit 43c60c3: revenueCurrency now used in markdown table headers. @codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43c60c3401

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +123 to +127
revenue: r.revenue,
revenuePerRecipient:
r.revenue !== undefined && r.sampleSize > 0
? r.revenue / r.sampleSize
: undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Propagate the revenue currency into built reports

When revenue-bearing results pass through the public buildExperimentReport path, this mapper copies the revenue amount but the returned report never assigns revenueCurrency, and TestResults has no currency field to supply it. Consequently, the new currency suffix in reportToMarkdown is unreachable without manually crafting or mutating an ExperimentReport, so normally generated revenue tables still display unitless monetary values; propagate a consistently validated currency through the builder input and output.

Useful? React with 👍 / 👎.

Comment thread packages/abtest/src/report.ts Outdated
Comment on lines +270 to +272
const hasRevenue = report.variants.some(
(v) => v.revenue !== undefined || v.revenuePerRecipient !== undefined,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Show revenue columns for a revenue primary metric

When a pre-registered test declares revenue_per_recipient but its results contain no revenue values—as happens with the current Listmonk metrics collectors—this condition is false, so the report identifies revenue as its primary metric while the table omits both revenue columns and shows only secondary rates. Include the revenue columns whenever the declared primary metric is revenue, rendering unavailable measurements as N/A rather than hiding the primary metric.

Useful? React with 👍 / 👎.

Addresses @codex P2 findings on PR #51:

- hasRevenue now also checks primaryMetric === 'revenue_per_recipient'
  so the table shows revenue columns even when no variant has revenue
  data yet (e.g. before metrics collection).
- buildExperimentReport sets revenueCurrency to undefined by default;
  callers that know the currency set it explicitly.
@imjlk

imjlk commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Commit 9f2ef54: hasRevenue checks primaryMetric; revenueCurrency propagated. @codex review

OpenCodeReview finding: the revenue visibility comment belonged in
reportToMarkdown, not in the buildExperimentReport return object.
@imjlk

imjlk commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Commit 596750c: removed misplaced comment. @codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 596750c1cb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const currencySuffix = report.revenueCurrency
? ` (${report.revenueCurrency})`
: "";
headers.push(`Revenue${currencySuffix}`, `Rev/Recipient${currencySuffix}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Document the new revenue report columns in both languages

This makes revenue and revenue-per-recipient columns part of the user-visible Markdown report, but neither README.md nor README_ko.md explains when they appear or how they are interpreted. The repository-wide AGENTS.md requires both English and Korean documentation to be updated for user-visible behavior changes.

AGENTS.md reference: AGENTS.md:L233-L237

Useful? React with 👍 / 👎.

}
: undefined,
preRegistration,
revenueCurrency: undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass currency into generated revenue reports

When buildExperimentReport() receives results containing revenue, it always sets revenueCurrency to undefined, and neither its arguments nor TestResults provide another path for supplying a currency. Consequently JSON omits the currency and Markdown renders monetary values under unlabeled Revenue and Rev/Recipient columns, so consumers cannot interpret or safely compare the amounts; carry the validated currency through the report input instead of discarding it here.

Useful? React with 👍 / 👎.

Addresses @codex P1 finding on PR #51: document the Revenue and
Rev/Recipient columns (with currency suffix) in root README.md and
README_ko.md.
@imjlk

imjlk commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Commit 76d61b1: documented revenue report columns in both root READMEs. @codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 76d61b1508

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@imjlk
imjlk merged commit 68a038c into main Jul 25, 2026
5 checks passed
@imjlk
imjlk deleted the fix/abtest-review-followup branch July 25, 2026 19:40
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.

1 participant