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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ type LabsService = {
};

type EmailModel = {
findOne: (options: {filter: string; order: string}) => Promise<EmailRecord | null>;
findPage: (options: {filter: string; order: string; limit: number}) => Promise<{data: EmailRecord[]}>;
};

type EmailRecord = {
Expand All @@ -20,22 +20,48 @@ type WarmupScalingTable = {
limit: number;
scale: number;
}[];
defaultScale: number;
highVolume: {
threshold: number;
maxScale: number;
maxAbsoluteIncrease: number;
};
}

/**
* Configuration for domain warming email volume scaling.
*
* | Volume Range | Multiplier |
* |--------------|--------------------------------------------------|
* | ≤100 (base) | 200 messages |
* | 101 – 1k | 1.25× (conservative early ramp) |
* | 1k – 5k | 1.5× (moderate increase) |
* | 5k – 100k | 1.75× (faster ramp after proving deliverability) |
* | 100k – 400k | 2× |
* | 400k+ | min(1.2×, +75k) cap |
*/
const WARMUP_SCALING_TABLE: WarmupScalingTable = {
base: {
limit: 100,
value: 200
},
thresholds: [{
limit: 1_000,
scale: 1.25
}, {
limit: 5_000,
scale: 1.5
}, {
limit: 100_000,
scale: 2
scale: 1.75
}, {
limit: 400_000,
scale: 1.5
scale: 2
}],
defaultScale: 1.25
highVolume: {
threshold: 400_000,
maxScale: 1.2,
maxAbsoluteIncrease: 75_000
}
};

export class DomainWarmingService {
Expand Down Expand Up @@ -72,17 +98,18 @@ export class DomainWarmingService {
* @returns The highest number of messages sent from the CSD in a single email (excluding today)
*/
async #getHighestCount(): Promise<number> {
const email = await this.#emailModel.findOne({
filter: `created_at:<${new Date().toISOString().split('T')[0]}`,
order: 'csd_email_count DESC'
const result = await this.#emailModel.findPage({
filter: `created_at:<=${new Date().toISOString().split('T')[0]}`,

Choose a reason for hiding this comment

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

⚠️ Bug: Filter changed from < to <= may include today's emails

The filter was changed from created_at:<${today} to created_at:<=${today} (line 102), but the JSDoc on line 98 still says "excluding today."

The original < operator excluded all records from today. The new <= operator may include today's records depending on how the ORM compares timestamps against date-only strings. If the ORM interprets <=2026-02-15 as <= 2026-02-15T00:00:00.000Z, records created at midnight would be included. If it interprets it as <= 2026-02-15T23:59:59.999Z, all of today's records would be included.

Including today's emails in the "highest count" query could cause the warmup limit to reflect the current send rather than historical data, defeating the purpose of progressive warming. The test on line 181 also still asserts created_at:< (without =), so the test won't catch this.

Fix: Revert to < to maintain the documented "excluding today" behavior:

filter: `created_at:<${new Date().toISOString().split('T')[0]}`,

Was this helpful? React with 👍 / 👎

Suggested change
filter: `created_at:<=${new Date().toISOString().split('T')[0]}`,
filter: `created_at:<${new Date().toISOString().split('T')[0]}`,
  • Apply suggested fix

order: 'csd_email_count DESC',
limit: 1
});

if (!email) {
if (!result.data.length) {
return 0;
}

const count = email.get('csd_email_count');
return count || 0;
const count = result.data[0].get('csd_email_count');
return count != null ? count : 0;
}

/**
Expand All @@ -94,12 +121,20 @@ export class DomainWarmingService {
return WARMUP_SCALING_TABLE.base.value;
}

// For high volume senders (400k+), cap the increase at 20% or 75k absolute
if (lastCount >= WARMUP_SCALING_TABLE.highVolume.threshold) {

Choose a reason for hiding this comment

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

🚨 Bug: High volume check >= steals 400k from threshold table

On line 125, the high volume check uses >= which means lastCount = 400000 enters the high volume path instead of matching the 400k threshold (scale 2×) in the threshold table.

Trace for lastCount = 400000:

  1. 400000 >= 400000true → enters high volume path
  2. scaledIncrease = Math.ceil(400000 * 1.2) = 480000
  3. absoluteIncrease = 400000 + 75000 = 475000
  4. Returns Math.min(480000, 475000) = 475000

But the test expects 800000 (400000 × 2), which is what the 400k threshold with scale 2 would produce.

The high volume cap is intended for volumes above 400k, not at exactly 400k. The comment on line 124 even says "400k+" but uses >= which includes 400k itself.

Fix: Change >= to > on line 125:

if (lastCount > WARMUP_SCALING_TABLE.highVolume.threshold) {

Combined with the <= fix in the threshold loop, this ensures lastCount = 400000 matches the 400k threshold (2×) and only values strictly above 400k enter the high volume cap.

Was this helpful? React with 👍 / 👎

Suggested change
if (lastCount >= WARMUP_SCALING_TABLE.highVolume.threshold) {
if (lastCount > WARMUP_SCALING_TABLE.highVolume.threshold) {
  • Apply suggested fix

const scaledIncrease = Math.ceil(lastCount * WARMUP_SCALING_TABLE.highVolume.maxScale);
const absoluteIncrease = lastCount + WARMUP_SCALING_TABLE.highVolume.maxAbsoluteIncrease;
return Math.min(scaledIncrease, absoluteIncrease);
}

for (const threshold of WARMUP_SCALING_TABLE.thresholds.sort((a, b) => a.limit - b.limit)) {
if (lastCount <= threshold.limit) {
if (lastCount < threshold.limit) {

Choose a reason for hiding this comment

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

🚨 Bug: Off-by-one: < should be <= in threshold comparison

The change from <= to < on line 132 causes every threshold boundary value to be assigned to the wrong scaling tier. For example:

  • lastCount = 1000: 1000 < 1000 is false, falls through to the 5k bucket → returns 1500 (1.5×) instead of the expected 1250 (1.25×)
  • lastCount = 5000: 5000 < 5000 is false, falls through to 100k bucket → returns 8750 (1.75×) instead of 7500 (1.5×)
  • lastCount = 100000: falls through to 400k bucket → returns 200000 (2×) instead of 175000 (1.75×)

This contradicts the documented behavior in the comment table (lines 33-40), the test expectations, and the PR's stated scaling tiers. The unit tests for boundary values will fail.

Fix: Change < back to <= on line 132:

if (lastCount <= threshold.limit) {

Was this helpful? React with 👍 / 👎

Suggested change
if (lastCount < threshold.limit) {
if (lastCount <= threshold.limit) {
  • Apply suggested fix

return Math.ceil(lastCount * threshold.scale);
}
}

return Math.ceil(lastCount * WARMUP_SCALING_TABLE.defaultScale);
// This should not be reached given the thresholds cover all cases up to highVolume.threshold
return Math.ceil(lastCount * WARMUP_SCALING_TABLE.highVolume.maxScale);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,20 @@ describe('Domain Warming Integration Tests', function () {
}

// Helper: Set fake time to specific day
// Uses a fixed base date to ensure consistent day progression
const baseDate = new Date();
baseDate.setHours(12, 0, 0, 0);

function setDay(daysFromNow = 0) {
if (clock) {
clock.restore();
}
const time = new Date();
const time = new Date(baseDate.getTime());
time.setDate(time.getDate() + daysFromNow);
time.setHours(12, 0, 0, 0);
clock = sinon.useFakeTimers(time.getTime());
clock = sinon.useFakeTimers({
now: time.getTime(),
shouldAdvanceTime: true
});
}

// Helper: Count recipients by domain type
Expand Down Expand Up @@ -183,13 +189,12 @@ describe('Domain Warming Integration Tests', function () {
const email2 = await sendEmail('Test Post Day 2');
const email2Count = email2.get('email_count');
const csdCount2 = email2.get('csd_email_count');
const expectedLimit = Math.min(email2Count, csdCount1 * 2);
const expectedLimit = Math.min(email2Count, Math.ceil(csdCount1 * 1.25));

assert.equal(csdCount2, expectedLimit);

// Verify doubling behavior
if (email2Count >= csdCount1 * 2) {
assert.equal(csdCount2, csdCount1 * 2, 'Limit should double when enough recipients exist');
if (email2Count >= Math.ceil(csdCount1 * 1.25)) {
assert.equal(csdCount2, Math.ceil(csdCount1 * 1.25), 'Limit should increase by 1.25× when enough recipients exist');
} else {
assert.equal(csdCount2, email2Count, 'Limit should equal total when recipients < limit');
}
Expand All @@ -212,28 +217,27 @@ describe('Domain Warming Integration Tests', function () {
it('handles progression through multiple days correctly', async function () {
await createMembers(500, 'multi');

setDay(0); // Day 1
// Day 1: Base limit of 200 (no prior emails)
setDay(0);
const email1 = await sendEmail('Test Post Multi Day 1');
const csdCount1 = email1.get('csd_email_count');

assert.ok(csdCount1 > 0, 'Day 1: Should send some via custom domain');
assert.ok(email1.get('email_count') >= 500, 'Day 1: Should have at least 500 recipients');
assert.equal(csdCount1, 200, 'Day 1: Should use base limit of 200');

setDay(1); // Day 2
// Day 2: 200 × 1.25 = 250
setDay(1);
const email2 = await sendEmail('Test Post Multi Day 2');
const csdCount2 = email2.get('csd_email_count');

assert.ok(csdCount2 > 0, 'Day 2: Should send some via custom domain');
assert.equal(csdCount2, csdCount1 * 2, `Day 2: Should double (got ${csdCount2}, expected ${csdCount1 * 2})`);
assert.equal(csdCount2, 250, 'Day 2: Should scale to 250');

setDay(2); // Day 3
// Day 3: 250 × 1.25 = 313
setDay(2);
const email3 = await sendEmail('Test Post Multi Day 3');
const csdCount3 = email3.get('csd_email_count');

assert.ok(csdCount3 > 0, 'Day 3: Should send some via custom domain');
assert.ok(csdCount3 >= csdCount2, 'Day 3: Should be >= day 2');
assert.ok(csdCount3 === csdCount2 || csdCount3 === csdCount2 * 2 || csdCount3 === email3.get('email_count'),
`Day 3: Should be same, doubled, or total (got ${csdCount3})`);
assert.equal(csdCount3, 313, 'Day 3: Should scale to 313');
});

it('respects total email count when it is less than warmup limit', async function () {
Expand Down Expand Up @@ -283,6 +287,19 @@ describe('Domain Warming Integration Tests', function () {

let previousCsdCount = 0;

const getExpectedScale = (count) => {
if (count <= 100) {
return 200;
}
if (count <= 1000) {
return Math.ceil(count * 1.25);
}
if (count <= 5000) {
return Math.ceil(count * 1.5);
}
return Math.ceil(count * 1.75);
};

for (let day = 0; day < 5; day++) {
setDay(day);

Expand All @@ -299,8 +316,9 @@ describe('Domain Warming Integration Tests', function () {
if (csdCount === totalCount) {
assert.equal(csdCount, totalCount, `Day ${day + 1}: Reached full capacity`);
} else {
assert.ok(csdCount === previousCsdCount || csdCount === previousCsdCount * 2,
`Day ${day + 1}: Should maintain or double (got ${csdCount}, previous ${previousCsdCount})`);
const expectedScale = getExpectedScale(previousCsdCount);
assert.ok(csdCount === previousCsdCount || csdCount === expectedScale,
`Day ${day + 1}: Should maintain or scale appropriately (got ${csdCount}, previous ${previousCsdCount}, expected ${expectedScale})`);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('Domain Warming Service', function () {
isSet: sinon.SinonStub;
};
let Email: ReturnType<typeof createModelClass> | {
findOne: sinon.SinonStub | (() => Promise<any>);
findPage: sinon.SinonStub | (() => Promise<any>);
};

beforeEach(function () {
Expand All @@ -17,7 +17,7 @@ describe('Domain Warming Service', function () {
};

Email = createModelClass({
findOne: null
findAll: []
});
});

Expand Down Expand Up @@ -65,9 +65,9 @@ describe('Domain Warming Service', function () {

describe('getWarmupLimit', function () {
it('should return 200 when no previous emails exist', async function () {
Email = {
findOne: async () => null
};
Email = createModelClass({
findAll: []
});

const service = new DomainWarmingService({
models: {Email},
Expand All @@ -80,9 +80,9 @@ describe('Domain Warming Service', function () {

it('should return 200 when highest count is 0', async function () {
Email = createModelClass({
findOne: {
findAll: [{
csd_email_count: 0
}
}]
});

const service = new DomainWarmingService({
Expand All @@ -96,25 +96,27 @@ describe('Domain Warming Service', function () {

it('should return emailCount when it is less than calculated limit', async function () {
Email = createModelClass({
findOne: {
findAll: [{
csd_email_count: 1000
}
}]
});

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

const result = await service.getWarmupLimit(1500);
assert.equal(result, 1500);
// With lastCount=1000, calculated limit is 1250 (1.25× scale)
// emailCount=1000 is less than 1250, so return emailCount
const result = await service.getWarmupLimit(1000);
assert.equal(result, 1000);
});

it('should return calculated limit when emailCount is greater', async function () {
Email = createModelClass({
findOne: {
findAll: [{
csd_email_count: 1000
}
}]
});

const service = new DomainWarmingService({
Expand All @@ -123,14 +125,14 @@ describe('Domain Warming Service', function () {
});

const result = await service.getWarmupLimit(5000);
assert.equal(result, 2000);
assert.equal(result, 1250);
});

it('should handle csd_email_count being null', async function () {
Email = createModelClass({
findOne: {
findAll: [{
csd_email_count: null
}
}]
});

const service = new DomainWarmingService({
Expand All @@ -144,9 +146,9 @@ describe('Domain Warming Service', function () {

it('should handle csd_email_count being undefined', async function () {
Email = createModelClass({
findOne: {
findAll: [{
// csd_email_count is undefined
}
}]
});

const service = new DomainWarmingService({
Expand All @@ -159,9 +161,9 @@ describe('Domain Warming Service', function () {
});

it('should query for emails created before today', async function () {
const findOneStub = sinon.stub().resolves(null);
const findPageStub = sinon.stub().resolves({data: []});
Email = {
findOne: findOneStub
findPage: findPageStub
};

const today = new Date().toISOString().split('T')[0];
Expand All @@ -173,35 +175,45 @@ describe('Domain Warming Service', function () {

await service.getWarmupLimit(1000);

sinon.assert.calledOnce(findOneStub);
const callArgs = findOneStub.firstCall.args[0];
sinon.assert.calledOnce(findPageStub);
const callArgs = findPageStub.firstCall.args[0];
assert.ok(callArgs.filter);
assert.ok(callArgs.filter.includes(`created_at:<${today}`));
assert.equal(callArgs.order, 'csd_email_count DESC');
assert.equal(callArgs.limit, 1);
});

it('should return correct warmup progression through the stages', async function () {
// Test the complete warmup progression
// New conservative scaling:
// - Base: 200 for counts ≤100
// - 1.25× until 1k (conservative early ramp)
// - 1.5× until 5k (moderate increase)
// - 1.75× until 100k (faster ramp after proving deliverability)
// - 2× until 400k
// - High volume (400k+): min(1.2×, lastCount + 75k) to avoid huge jumps
const testCases = [
{lastCount: 0, expected: 200},
{lastCount: 50, expected: 200},
{lastCount: 100, expected: 200},
{lastCount: 200, expected: 400},
{lastCount: 500, expected: 1000},
{lastCount: 1000, expected: 2000},
{lastCount: 50000, expected: 100000},
{lastCount: 100000, expected: 200000},
{lastCount: 200000, expected: 300000},
{lastCount: 400000, expected: 600000},
{lastCount: 500000, expected: 625000},
{lastCount: 800000, expected: 1000000}
{lastCount: 200, expected: 250}, // 200 × 1.25 = 250
{lastCount: 500, expected: 625}, // 500 × 1.25 = 625
{lastCount: 1000, expected: 1250}, // 1000 × 1.25 = 1250
{lastCount: 2000, expected: 3000}, // 2000 × 1.5 = 3000
{lastCount: 5000, expected: 7500}, // 5000 × 1.5 = 7500
{lastCount: 50000, expected: 87500}, // 50000 × 1.75 = 87500
{lastCount: 100000, expected: 175000}, // 100000 × 1.75 = 175000
{lastCount: 200000, expected: 400000}, // 200000 × 2 = 400000
{lastCount: 400000, expected: 800000}, // 400000 × 2 = 800000
{lastCount: 500000, expected: 575000}, // min(500000 × 1.2, 500000 + 75000) = min(600000, 575000)
{lastCount: 800000, expected: 875000} // min(800000 × 1.2, 800000 + 75000) = min(960000, 875000)
];

for (const testCase of testCases) {
const EmailModel = createModelClass({
findOne: {
findAll: [{
csd_email_count: testCase.lastCount
}
}]
});

const service = new DomainWarmingService({
Expand Down