Skip to content

Comments

Switched to time-based domain warmup#20

Open
tomerqodo wants to merge 2 commits intocoderabbit_combined_20260121_qodo_grep_cursor_copilot_base_switched_to_time-based_domain_warmup_pr236from
coderabbit_combined_20260121_qodo_grep_cursor_copilot_head_switched_to_time-based_domain_warmup_pr236
Open

Switched to time-based domain warmup#20
tomerqodo wants to merge 2 commits intocoderabbit_combined_20260121_qodo_grep_cursor_copilot_base_switched_to_time-based_domain_warmup_pr236from
coderabbit_combined_20260121_qodo_grep_cursor_copilot_head_switched_to_time-based_domain_warmup_pr236

Conversation

@tomerqodo
Copy link

@tomerqodo tomerqodo commented Jan 21, 2026

Benchmark PR from qodo-benchmark#236

Summary by CodeRabbit

  • Refactor
    • Improved email domain warmup system to use time-based progression instead of count-based scaling. Default warmup configuration now ramps from 200 to 200,000 emails over 42 days using a more predictable exponential curve.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

Walkthrough

The pull request replaces the email domain warming service's scaling-based model with a time-based progression system. The DomainWarmingService now uses a configuration object defining start volume, end volume, and total warmup days to compute warmup limits via exponential ramp rather than previous count-based thresholds. Tests are refactored to validate the new time-based approach.

Changes

Cohort / File(s) Summary
Service Implementation
ghost/core/core/server/services/email-service/DomainWarmingService.ts
Introduced WarmupVolumeOptions type and DefaultWarmupOptions constant (start: 200, end: 200000, totalDays: 42). Replaced count-based scaling logic with day-based exponential ramp calculation. Added #getDaysSinceFirstEmail() method to compute progression based on first email creation date. Removed previous #getHighestCount() and #getTargetLimit() methods.
Integration Tests
ghost/core/test/integration/services/email-service/domain-warming.test.js
Replaced hard-coded scaling multipliers (1.25×) with time-based expected limit calculations. Introduced getExpectedLimit() helper and removed getExpectedScale(). Updated assertions to verify warmup progression aligns with new time-based formula across multiple day scenarios.
Unit Tests
ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts
Added sinon.useFakeTimers() for controlled time-based testing. Introduced daysAgo() helper for relative date simulation. Reworked test scenarios from lastCount-based progression to day-based progression (day 0 through day 42+). Added tests validating first email date retrieval and Infinity return after warmup completion.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 Once counting each message sent off with care,
Now time charts the journey through exponential air,
From start humble hundreds to thousands so grand,
In forty-two days the warmth expands,
A temporal warmup, day by day we advance! 🐇✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'Switched to time-based domain warmup' directly and clearly summarizes the main change: replacing the previous warmup logic with a time-based approach.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@ghost/core/core/server/services/email-service/DomainWarmingService.ts`:
- Around line 83-98: The getWarmupLimit method is missing semicolons on several
statements causing ESLint failures; update the function (method getWarmupLimit
and any statements using this.#getDaysSinceFirstEmail() and the const limit
calculation that references this.#warmupConfig) to terminate each statement with
a semicolon (after the const day assignment, after the if block return Infinity
line, and after the return Math.min(emailCount, limit) line) so the file
conforms to the project's semicolon linting rule.

In `@ghost/core/test/integration/services/email-service/domain-warming.test.js`:
- Around line 298-303: The test helper getExpectedLimit uses Math.round but
DomainWarmingService.getWarmupLimit uses Math.floor, causing off-by-one
mismatches; update getExpectedLimit to use Math.floor (keep same
start/end/totalDays formula) so the helper mirrors
DomainWarmingService.getWarmupLimit's rounding behavior when calculating
expected limits.

In
`@ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts`:
- Around line 284-301: The test expects warmup to be complete at 42 days but the
implementation in DomainWarmingService uses a strict greater-than check (day >
this.#warmupConfig.totalDays), causing day=42 to miss the completion branch;
update the boundary check in getWarmupLimit to use >=
this.#warmupConfig.totalDays (or equivalent) so that when the elapsed days is
equal to warmupConfig.totalDays the method returns Infinity as the test expects.
🧹 Nitpick comments (1)
ghost/core/core/server/services/email-service/DomainWarmingService.ts (1)

64-76: Consider Math.floor instead of Math.ceil for day calculation.

Using Math.ceil means an email sent a few hours ago counts as "1 day ago," which could cause warmup to progress faster than intended. For example, an email sent 8 hours ago would yield Math.ceil(0.33) = 1.

Consider whether Math.floor better matches the business intent of counting complete days elapsed.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 71dbb47 and 1b93c8a.

📒 Files selected for processing (3)
  • ghost/core/core/server/services/email-service/DomainWarmingService.ts
  • ghost/core/test/integration/services/email-service/domain-warming.test.js
  • ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts
🧰 Additional context used
🧬 Code graph analysis (2)
ghost/core/test/integration/services/email-service/domain-warming.test.js (1)
ghost/core/test/unit/server/services/email-service/batch-sending-service.test.js (4)
  • sendEmail (96-102)
  • sendEmail (125-131)
  • sendEmail (152-164)
  • sendEmail (193-199)
ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts (1)
ghost/core/core/server/services/email-service/DomainWarmingService.ts (1)
  • DomainWarmingService (30-99)
🪛 ESLint
ghost/core/core/server/services/email-service/DomainWarmingService.ts

[error] 84-85: Missing semicolon.

(semi)


[error] 86-87: Missing semicolon.

(semi)


[error] 95-96: Missing semicolon.

(semi)

🔇 Additional comments (7)
ghost/core/core/server/services/email-service/DomainWarmingService.ts (2)

85-87: Potential off-by-one in warmup completion check.

The condition day > this.#warmupConfig.totalDays returns Infinity starting from day 43, not day 42. With totalDays=42:

  • Day 41: calculates limit (200,000 - the end value)
  • Day 42: 42 > 42 is false, so it calculates 200 * 1000^(42/41) ≈ 236,798 (exceeds configured end)
  • Day 43: returns Infinity

If the intent is that warmup completes after 42 days (days 0–41), the condition should be >=:

Suggested fix if warmup should complete on day 42
-        if (day > this.#warmupConfig.totalDays) {
+        if (day >= this.#warmupConfig.totalDays) {

Please verify the intended behavior: should day 42 return Infinity or calculate a limit exceeding the configured end value?


18-28: LGTM on the warmup configuration design.

The WarmupVolumeOptions type and DefaultWarmupOptions constant provide a clean, configurable approach to the time-based warmup. The exponential formula start * (end/start)^(day/(totalDays-1)) creates a smooth progression from 200 to 200,000 over the warmup period.

ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts (2)

109-115: Well-structured test helper and time management.

The daysAgo helper and fake timer setup provide deterministic test behavior. The approach of fixing the clock to a specific date and computing relative dates ensures consistent results across test runs.


248-282: Comprehensive day progression test coverage.

The progression test thoroughly validates the exponential formula across multiple days (0, 1, 5, 10, 20, 21, 30, 40, 41) with mathematically verified expected values. Good inclusion of the error message showing both expected and actual values for debugging.

ghost/core/test/integration/services/email-service/domain-warming.test.js (3)

166-184: Good integration test for initial warmup behavior.

The test properly validates that when total recipients (104) are below the warmup limit (200), all emails go through the custom domain. The assertions verify both the email model counts and the actual batch distribution.


109-163: Thorough test cleanup ensures isolation.

The afterEach cleanup properly handles clock restoration, stub cleanup, and database cleanup for test-created members, emails, batches, and recipients. This prevents test pollution and maintains reliable test runs.


220-247: Multi-day progression test validates time-based warmup correctly.

The test verifies the exponential growth formula across days 0, 1, and 2 with specific expected limits (200, 237, 280). Comments clearly document the formula being tested.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +83 to 98
async getWarmupLimit(emailCount: number): Promise<number> {
const day = await this.#getDaysSinceFirstEmail()
if (day > this.#warmupConfig.totalDays) {
return Infinity
}

for (const threshold of WARMUP_SCALING_TABLE.thresholds.sort((a, b) => a.limit - b.limit)) {
if (lastCount <= threshold.limit) {
return Math.ceil(lastCount * threshold.scale);
}
}
const limit = Math.floor(
this.#warmupConfig.start *
Math.pow(
this.#warmupConfig.end / this.#warmupConfig.start,
day / (this.#warmupConfig.totalDays - 1)
)
)

// This should not be reached given the thresholds cover all cases up to highVolume.threshold
return Math.ceil(lastCount * WARMUP_SCALING_TABLE.highVolume.maxScale);
return Math.min(emailCount, limit)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing semicolons flagged by ESLint.

Lines 84, 86, and 95 are missing semicolons, which violates the project's linting rules.

Proposed fix
     async getWarmupLimit(emailCount: number): Promise<number> {
-        const day = await this.#getDaysSinceFirstEmail()
+        const day = await this.#getDaysSinceFirstEmail();
         if (day > this.#warmupConfig.totalDays) {
-            return Infinity
+            return Infinity;
         }

         const limit = Math.floor(
             this.#warmupConfig.start *
             Math.pow(
                 this.#warmupConfig.end / this.#warmupConfig.start,
                 day / (this.#warmupConfig.totalDays - 1)
             )
-        )
+        );

-        return Math.min(emailCount, limit)
+        return Math.min(emailCount, limit);
     }
🧰 Tools
🪛 ESLint

[error] 84-85: Missing semicolon.

(semi)


[error] 86-87: Missing semicolon.

(semi)


[error] 95-96: Missing semicolon.

(semi)


[error] 97-98: Missing semicolon.

(semi)

🤖 Prompt for AI Agents
In `@ghost/core/core/server/services/email-service/DomainWarmingService.ts` around
lines 83 - 98, The getWarmupLimit method is missing semicolons on several
statements causing ESLint failures; update the function (method getWarmupLimit
and any statements using this.#getDaysSinceFirstEmail() and the const limit
calculation that references this.#warmupConfig) to terminate each statement with
a semicolon (after the const day assignment, after the if block return Infinity
line, and after the return Math.min(emailCount, limit) line) so the file
conforms to the project's semicolon linting rule.

Comment on lines +298 to 303
const getExpectedLimit = (day) => {
const start = 200;
const end = 200000;
const totalDays = 42;
return Math.round(start * Math.pow(end / start, day / (totalDays - 1)));
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent rounding: Math.round vs implementation's Math.floor.

The getExpectedLimit helper uses Math.round, but the actual DomainWarmingService.getWarmupLimit implementation uses Math.floor. This could cause test failures for edge cases where the calculated value has a fractional part near 0.5.

Proposed fix to match implementation
             const getExpectedLimit = (day) => {
                 const start = 200;
                 const end = 200000;
                 const totalDays = 42;
-                return Math.round(start * Math.pow(end / start, day / (totalDays - 1)));
+                return Math.floor(start * Math.pow(end / start, day / (totalDays - 1)));
             };
📝 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 getExpectedLimit = (day) => {
const start = 200;
const end = 200000;
const totalDays = 42;
return Math.round(start * Math.pow(end / start, day / (totalDays - 1)));
};
const getExpectedLimit = (day) => {
const start = 200;
const end = 200000;
const totalDays = 42;
return Math.floor(start * Math.pow(end / start, day / (totalDays - 1)));
};
🤖 Prompt for AI Agents
In `@ghost/core/test/integration/services/email-service/domain-warming.test.js`
around lines 298 - 303, The test helper getExpectedLimit uses Math.round but
DomainWarmingService.getWarmupLimit uses Math.floor, causing off-by-one
mismatches; update getExpectedLimit to use Math.floor (keep same
start/end/totalDays formula) so the helper mirrors
DomainWarmingService.getWarmupLimit's rounding behavior when calculating
expected limits.

Comment on lines +284 to +301
it('should return Infinity after warmup period is complete', async function () {
// After 42 days, warmup is complete
Email = createModelClass({
findAll: [{
csd_email_count: 100,
created_at: daysAgo(42)
}]
});

const service = new DomainWarmingService({
models: {Email},
labs,
config
});

const result = await service.getWarmupLimit(1000000);
assert.equal(result, Infinity);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find the domain-warming-service implementation file
find . -name "domain-warming-service.ts" -type f | grep -v test | head -5

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 66


🏁 Script executed:

# Verify the exact boundary condition in the implementation
ast-grep --pattern 'if (day > this.#warmupConfig.totalDays) {
  $$$
}'

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 390


🏁 Script executed:

# Also check for >= condition as alternative
ast-grep --pattern 'if (day >= this.#warmupConfig.totalDays) {
  $$$
}'

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 66


🏁 Script executed:

# Search the test file for warmupConfig or totalDays configuration
rg -A 5 -B 5 "warmupConfig|totalDays" ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 681


🏁 Script executed:

# Also check the implementation to see the default warmupConfig
cat -n ghost/core/core/server/services/email-service/DomainWarmingService.ts | head -100

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 3620


🏁 Script executed:

# Look at the full test file around the boundary test to see context
sed -n '270,310p' ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts

Repository: qodo-benchmark/Ghost-combined-coderabbit

Length of output: 1491


Test expectation at day 42 conflicts with implementation boundary condition.

The test expects Infinity when the first email was 42 days ago. However, the implementation uses day > this.#warmupConfig.totalDays where totalDays is 42, meaning 42 > 42 evaluates to false. The test will fail because a calculated limit will be returned instead of Infinity.

The implementation condition should be >= to match the test assertion that "after 42 days, warmup is complete."

🤖 Prompt for AI Agents
In
`@ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts`
around lines 284 - 301, The test expects warmup to be complete at 42 days but
the implementation in DomainWarmingService uses a strict greater-than check (day
> this.#warmupConfig.totalDays), causing day=42 to miss the completion branch;
update the boundary check in getWarmupLimit to use >=
this.#warmupConfig.totalDays (or equivalent) so that when the elapsed days is
equal to warmupConfig.totalDays the method returns Infinity as the test expects.

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