Skip to content

Switched to time-based domain warmup#47

Open
tomerqodo wants to merge 2 commits intocoderabbit_full_base_switched_to_time-based_domain_warmup_pr9from
coderabbit_full_head_switched_to_time-based_domain_warmup_pr9
Open

Switched to time-based domain warmup#47
tomerqodo wants to merge 2 commits intocoderabbit_full_base_switched_to_time-based_domain_warmup_pr9from
coderabbit_full_head_switched_to_time-based_domain_warmup_pr9

Conversation

@tomerqodo
Copy link

@tomerqodo tomerqodo commented Jan 28, 2026

Benchmark PR from agentic-review-benchmarks#9

Summary by CodeRabbit

Release Notes

  • Improvements
    • Updated domain warming calculation to use a consistent time-based progression model that runs over a fixed 42-day period, progressively scaling from 200 to 200,000 emails per day.

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Walkthrough

The domain warming service refactors from a rigid lookup table model to a dynamic time-based exponential growth algorithm. The warmup limit now scales exponentially between configured start and end values over a specified number of days, computed from elapsed time since the first email.

Changes

Cohort / File(s) Summary
Domain Warming Service Implementation
ghost/core/core/server/services/email-service/DomainWarmingService.ts
Replaced WarmupScalingTable with WarmupVolumeOptions type and DefaultWarmupOptions constant (start: 200, end: 200000, totalDays: 42). Added #warmupConfig field and #getDaysSinceFirstEmail method to calculate elapsed days since first email. Reworked getWarmupLimit to apply exponential growth formula: start × (end/start)^(day/(totalDays-1)) clamped by emailCount and totalDays boundaries. Constructor expanded to accept labs and config dependencies.
Integration Tests
ghost/core/test/integration/services/email-service/domain-warming.test.js
Updated test expectations to reflect time-based warmup progression. Introduced getExpectedLimit(day) helper function implementing the exponential growth formula. Replaced per-day branching logic with single formula-based assertions. Aligned test day labels (Day 0, Day 1, etc.) with time-based calculation model.
Unit Tests
ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts
Added clock/fake-timers setup for deterministic time-based testing. Introduced daysAgo(days) helper to generate relative timestamps. Expanded getWarmupLimit test suite with comprehensive day-based scenarios (Day 0, 21, 41/42, 100). Updated query assertions to expect csd_email_count:-null filter and created_at ASC sort order. Replaced numeric ramp expectations with time-driven progression values.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Days count down, exponentials climb,
From humble 200 to thousands sublime,
Forty-two sunrises fuel the ascent,
Time-based warmth—perfectly spent!

🚥 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 title 'Switched to time-based domain warmup' directly summarizes the main change: replacing a step-table based warmup configuration with a time-based approach using days elapsed. This is the primary architectural change across all modified files.
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: 2

🤖 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 causing
ESLint style failures; update the function (DomainWarmingService.getWarmupLimit)
to add semicolons after the const day declaration, after the if-block's return
Infinity statement, and after the const limit assignment/return lines so that
statements like "const day = await this.#getDaysSinceFirstEmail()", the limit
calculation assignment, and the final "return Math.min(emailCount, limit)" end
with semicolons; ensure all newly added semicolons match the codebase style.

In `@ghost/core/test/integration/services/email-service/domain-warming.test.js`:
- Around line 296-303: The test helper getExpectedLimit uses Math.round but the
implementation in DomainWarmingService.getWarmupLimit uses Math.floor, causing
possible mismatches; update the helper getExpectedLimit to use Math.floor (same
start/end/totalDays formula) so both functions compute limits identically, and
run the test to ensure assertions align with
DomainWarmingService.getWarmupLimit.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1f554a3 and 5f87951.

📒 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 (1)
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 (10)
ghost/core/test/unit/server/services/email-service/domain-warming-service.test.ts (5)

16-38: LGTM! Proper fake timer setup for deterministic tests.

The clock initialization with a fixed date and proper cleanup in afterEach ensures consistent, reproducible test results for the time-based warmup logic.


110-115: LGTM! Clean helper for generating test dates.

The daysAgo helper correctly uses the same fixed date as the fake timer, ensuring consistent day calculations across all tests.


248-282: LGTM! Comprehensive warmup progression test.

The test cases cover the full exponential curve from start (200) to end (200000) with well-chosen intermediate checkpoints. The expected values align with the formula start * (end/start)^(day/(totalDays-1)).


284-320: LGTM! Good boundary and post-warmup coverage.

Testing both the exact boundary (day 42) and a value well beyond (day 100) ensures the Infinity return is robust.


226-246: LGTM! Query behavior verification is correct.

The test properly validates the filter, ordering, and limit parameters for fetching the first qualifying email.

ghost/core/core/server/services/email-service/DomainWarmingService.ts (2)

18-28: LGTM! Clean warmup configuration type and defaults.

The WarmupVolumeOptions type and DefaultWarmupOptions constant provide a clear, configurable foundation for the time-based warmup algorithm.


64-76: LGTM! Day calculation logic is sound.

Using Math.ceil ensures that even partial days count as a full day elapsed, which is appropriate for a warmup progression that should advance once any time has passed since the first email.

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

199-207: LGTM! Day 2 test correctly uses time-based warmup formula.

The expected limit of 237 for day 1 aligns with the exponential formula and unit test expectations.


223-247: LGTM! Multi-day progression test validates the exponential curve.

The expected values for days 0, 1, and 2 are mathematically correct and consistent with the unit tests.


305-323: LGTM! Comprehensive maximum limit test loop.

The 5-day progression test with non-decreasing validation is well-structured. Just ensure the getExpectedLimit helper is fixed per the previous comment.

✏️ 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

Add missing semicolons for code style consistency.

ESLint flags missing semicolons at lines 84, 86, and 95. While JavaScript/TypeScript allows semicolon omission, the codebase appears to use semicolons consistently.

🔧 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);
     }
📝 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
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)
}
async getWarmupLimit(emailCount: number): Promise<number> {
const day = await this.#getDaysSinceFirstEmail();
if (day > this.#warmupConfig.totalDays) {
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);
}
🧰 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 causing ESLint
style failures; update the function (DomainWarmingService.getWarmupLimit) to add
semicolons after the const day declaration, after the if-block's return Infinity
statement, and after the const limit assignment/return lines so that statements
like "const day = await this.#getDaysSinceFirstEmail()", the limit calculation
assignment, and the final "return Math.min(emailCount, limit)" end with
semicolons; ensure all newly added semicolons match the codebase style.

Comment on lines +296 to 303
// Time-based warmup: limit = start * (end/start)^(day/(totalDays-1))
// With start=200, end=200000, totalDays=42
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 Math.floor in implementation.

The getExpectedLimit helper uses Math.round, but the actual DomainWarmingService.getWarmupLimit implementation uses Math.floor. This could cause test assertions to fail when the fractional part is ≥ 0.5.

🔧 Proposed fix
             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
// Time-based warmup: limit = start * (end/start)^(day/(totalDays-1))
// With start=200, end=200000, totalDays=42
const getExpectedLimit = (day) => {
const start = 200;
const end = 200000;
const totalDays = 42;
return Math.round(start * Math.pow(end / start, day / (totalDays - 1)));
};
// Time-based warmup: limit = start * (end/start)^(day/(totalDays-1))
// With start=200, end=200000, totalDays=42
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 296 - 303, The test helper getExpectedLimit uses Math.round but the
implementation in DomainWarmingService.getWarmupLimit uses Math.floor, causing
possible mismatches; update the helper getExpectedLimit to use Math.floor (same
start/end/totalDays formula) so both functions compute limits identically, and
run the test to ensure assertions align with
DomainWarmingService.getWarmupLimit.

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