mirror of
https://github.com/warmbly/warmbly.git
synced 2026-11-04 16:14:00 +00:00
feat: align advisor throttling guidance and campaign hold actions with automatic placement recovery
This commit is contained in:
committed by
Devin AI
parent
d3612255bb
commit
67a2077ce0
@@ -111,7 +111,11 @@ Only the first two badge a nav tab.
|
||||
|
||||
**Mailbox configuration**: daily cap above the `50`/day safe band (gently with clean numbers, firmly without); a mailbox under `30` days old sending above `20`/day; a send gap under `5` minutes; unresolved errors on a mailbox campaigns still use; too few mailboxes carrying total volume; an inactive mailbox attached to a running campaign.
|
||||
|
||||
**Warmup**: warmup off while sending cold (critical on a new mailbox); warmup left paused; a ceiling too low to balance cold volume (roughly half the cold cap); reply rate under `20%`, making warmup one-way; quarantined or blocked from the pool; none of the pool's warmup mail to the mailbox arriving, which stops the pool sending to it (see [unreachable mailboxes](/guides/warmup/#mailboxes-the-pool-cannot-reach)). Missing or stale Cloud status is reported separately from a real reputation ban, with steps to refresh it without disconnecting mailboxes.
|
||||
**Warmup**: warmup off while sending cold (critical on a new mailbox); warmup left paused; a ceiling too low to balance cold volume (roughly half the cold cap); reply rate under `20%`, making warmup one-way; temporarily throttled sending, reported separately from quarantine or a pool block; none of the pool's warmup mail to the mailbox arriving, which stops the pool sending to it (see [unreachable mailboxes](/guides/warmup/#mailboxes-the-pool-cannot-reach)). Missing or stale Cloud status is reported separately from a real reputation ban, with steps to refresh it without disconnecting mailboxes.
|
||||
|
||||
A **throttled** mailbox remains in the warmup pool. The advisor explains the automatic reduced sending pace rather than asking you to remove it from campaigns, wait a fixed week or manually re-enter the pool. Keep warmup running and do not increase its cap to bypass the slowdown. For placement throttles, half volume steps down to watch below `15%` spam placement, and watch lifts below `7.5%`; separate ramp limits still apply. Re-entry probation and other cooldowns follow the reason shown on the mailbox. A throttle alone does not mean the connection has failed or the provider has blocked the mailbox.
|
||||
|
||||
The advisor's placement sample covers today and the preceding six UTC dates, matching the Deliverability display and placement-health decisions. After an upgrade, the next advisor evaluation replaces outdated throttle/exclusion guidance automatically. A campaign hold you already applied remains yours to release after checking any other findings.
|
||||
|
||||
**Campaigns**: no follow-ups; reply rate under `1%` at volume, escalating under `0.3%`; one step replying below a quarter of the campaign's best (compared only against siblings, which share an audience); one-click unsubscribe off; a daily limit above what its mailboxes can send; no active sender; follow-up spacing outside `2` to `14` days; under `7` days of leads left; no A/B test at high volume; a sending window too narrow for the volume; contacts enrolled in several running campaigns.
|
||||
|
||||
@@ -129,7 +133,7 @@ Two more copy findings run only when the operator has configured TypeSafe (`TYPE
|
||||
|
||||
Opening a recommendation walks three screens: **Why** (with the measured numbers), **What changes** (the exact before and after of every field), and **Done** (with **Undo** where revertible).
|
||||
|
||||
A finding that asks you to stop cold sending from a mailbox, for spam placement or a lost warmup pool standing, applies [Hold from campaigns](/guides/mailboxes/#holding-a-mailbox-yourself) rather than switching the mailbox off, and leaves its warmup as it is. It is offered only while campaigns can still pick the mailbox, so a mailbox that is already held, resting or switched off gets no fix, and **Undo** never releases a hold someone else set.
|
||||
A finding that asks you to stop cold sending from a mailbox, for severe spam placement (`50%` or more on at least `20` major-provider deliveries), quarantine or a pool block, applies [Hold from campaigns](/guides/mailboxes/#holding-a-mailbox-yourself) rather than switching the mailbox off, and leaves its warmup as it is. A recoverable placement slowdown below that severe band does not offer a campaign hold on its own; complaints, bounces and other confirmed problems are still checked separately. The hold is offered only while campaigns can still pick the mailbox, so a mailbox that is already held, resting or switched off gets no fix, and **Undo** never releases a hold someone else set.
|
||||
|
||||
Nothing is applied until you have seen the second screen. The change runs as you, with your permissions, and is written to the audit log. If you can see a finding but lack permission for its fix, applying it is refused rather than silently escalated.
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ func deliverabilityDetectors() []Detector {
|
||||
{
|
||||
Key: "mailbox_spam_placement",
|
||||
Category: models.AdvisorCategoryDeliverability,
|
||||
About: "The share of a mailbox's warmup mail that partner inboxes filed into spam. This is the earliest honest read on inbox placement, because it is measured on mail the platform controls end to end rather than inferred from opens.",
|
||||
About: "The share of a mailbox's warmup mail that major-provider inboxes filed into spam. At least 20 deliveries are needed. At 10% it warns and at 20% it escalates the automatic placement-based slowdown, not pool exclusion or a campaign hold. At 50% or more it recommends holding cold campaigns while keeping warmup running. Follow the recommended action in the evidence.",
|
||||
Run: detectSpamPlacement,
|
||||
},
|
||||
{
|
||||
@@ -169,14 +169,22 @@ func detectSpamPlacement(s *repository.AdvisorSnapshot) []Finding {
|
||||
}
|
||||
|
||||
severity := models.AdvisorMedium
|
||||
remedy := "Slow this mailbox down: lower the daily cap and widen the gap between sends while placement recovers. Warmup should keep running throughout."
|
||||
remedy := "Keep warmup running and do not increase sending while placement recovers. Warmbly adjusts the placement-health sending pace automatically; a temporary slowdown does not require a campaign hold or manual pool re-entry."
|
||||
steps := []string{
|
||||
"Review the domain's authentication results and investigate any confirmed failures. Spam placement alone does not prove an authentication problem.",
|
||||
"Do not raise the daily cap to bypass the slowdown. Warmbly manages placement-health sending reductions and recovery automatically.",
|
||||
"Keep warmup enabled so new deliveries can show whether placement is improving.",
|
||||
"Review recent campaign targeting and copy for poor audience fit, heavy link counts and missing unsubscribe headers.",
|
||||
"Follow the recent major-provider placement sample. A recoverable placement slowdown does not require a fixed week-long wait or manual pool re-entry.",
|
||||
}
|
||||
switch {
|
||||
case r >= spamPlacementBlock:
|
||||
severity = models.AdvisorCritical
|
||||
remedy = "Stop cold sending from this mailbox now. At this placement rate almost nothing is reaching an inbox, and every further send deepens the reputation hole. Leave warmup running and let it recover before resuming."
|
||||
case r >= spamPlacementQuarantine:
|
||||
remedy = "Stop cold sending from this mailbox now. At least half of its recent warmup deliveries at Google, Microsoft and Yahoo landed in spam. Leave warmup running, investigate the cause, and let placement recover before resuming."
|
||||
steps[1] = "Hold this mailbox out of campaigns while investigating its placement. Leave warmup enabled rather than switching the mailbox off."
|
||||
steps[4] = "Wait for a fresh sample showing recovered placement before releasing the campaign hold, then resume at a low daily cap."
|
||||
case r >= spamPlacementSlowdown:
|
||||
severity = models.AdvisorHigh
|
||||
remedy = "Take this mailbox out of cold rotation for a week. Keep warmup running, check SPF/DKIM/DMARC on the domain, and resume at a low cap once placement is back under 10%."
|
||||
}
|
||||
|
||||
f := Finding{
|
||||
@@ -194,25 +202,21 @@ func detectSpamPlacement(s *repository.AdvisorSnapshot) []Finding {
|
||||
"Over the last 7 days %d of %d warmup messages from %s delivered at Google, Microsoft and Yahoo went to spam. Warmup placement is the earliest honest read on where cold mail is landing, because it is measured on mail the platform controls end to end. Other mail hosts run their own filters and are not counted.",
|
||||
p.MajorSpam, p.MajorDelivered, m.Email),
|
||||
Remedy: remedy,
|
||||
Steps: []string{
|
||||
"Check SPF, DKIM and DMARC on this domain first. Authentication is the single biggest cause of spam placement, and no amount of volume tuning compensates for it.",
|
||||
"Lower this mailbox's daily cap and widen the gap between sends. Placement recovers on calm, spaced-out sending.",
|
||||
"Leave warmup running. It is what rebuilds the reputation, and stopping it now removes the only positive signal the mailbox is generating.",
|
||||
"Read the last few campaigns this mailbox sent for spam-trigger phrasing, heavy link counts, and missing unsubscribe headers.",
|
||||
"Give it a week and check this number again before putting the mailbox back into full rotation.",
|
||||
},
|
||||
Steps: steps,
|
||||
Evidence: map[string]any{
|
||||
"mailbox": m.Email,
|
||||
"major_provider_spam_7d": p.MajorSpam,
|
||||
"major_provider_delivered_7d": p.MajorDelivered,
|
||||
"spam_placement_percent": band(r),
|
||||
"quarantine_band_percent": spamPlacementQuarantine,
|
||||
"slowdown_band_percent": spamPlacementSlowdown,
|
||||
"hold_band_percent": spamPlacementBlock,
|
||||
"currently_sending_cold": m.SendingCold(),
|
||||
"current_daily_cap": m.CampaignLimit,
|
||||
"recommended_action": remedy,
|
||||
},
|
||||
}
|
||||
|
||||
if m.SendingCold() && r >= spamPlacementQuarantine {
|
||||
if m.SendingCold() && r >= spamPlacementBlock {
|
||||
f.Action = mailboxHoldAction(m.ID, "Hold this mailbox out of campaigns")
|
||||
}
|
||||
out = append(out, f)
|
||||
|
||||
@@ -2,6 +2,7 @@ package advisor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -265,6 +266,149 @@ func TestSpamPlacementRespectsItsSampleFloorAndBands(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlacementSlowdownDoesNotOfferACampaignHold(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
spam int
|
||||
severity models.AdvisorSeverity
|
||||
}{
|
||||
{10, models.AdvisorMedium},
|
||||
{19, models.AdvisorMedium},
|
||||
{20, models.AdvisorHigh},
|
||||
{49, models.AdvisorHigh},
|
||||
{50, models.AdvisorCritical},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%d percent", tc.spam), func(t *testing.T) {
|
||||
m := healthyMailbox()
|
||||
m.WarmupPlacement = models.WarmupPlacementEvidence{MajorDelivered: 100, MajorSpam: tc.spam}
|
||||
f := findingsByKey(Detect(snapshotOf(m), defaults()))["mailbox_spam_placement"]
|
||||
if f.Severity != tc.severity {
|
||||
t.Fatalf("severity = %s, want %s", f.Severity, tc.severity)
|
||||
}
|
||||
if tc.spam >= 50 {
|
||||
if f.Action == nil || f.Action.Tool != "set_mailbox_send_hold" || !strings.Contains(f.Remedy, "Stop cold sending") {
|
||||
t.Fatalf("severe placement lost its campaign hold: %+v", f)
|
||||
}
|
||||
return
|
||||
}
|
||||
if f.Action != nil || !strings.Contains(f.Remedy, "automatically") {
|
||||
t.Fatalf("recoverable placement should advise automatic slowdown without a hold: %+v", f)
|
||||
}
|
||||
if f.Evidence["recommended_action"] != f.Remedy {
|
||||
t.Fatal("narration evidence does not carry the current recovery guidance")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestThrottledMailboxAdviceKeepsItInThePool(t *testing.T) {
|
||||
for _, reason := range []string{
|
||||
"27.3% of 22 major-provider deliveries landed in spam",
|
||||
"re-entry probation after block expiry",
|
||||
} {
|
||||
t.Run(reason, func(t *testing.T) {
|
||||
m := healthyMailbox()
|
||||
m.PoolHealth, m.PoolHealthReason = "throttled", reason
|
||||
m.PoolBlocked = true // A throttle's future blocked_until is not pool exclusion.
|
||||
m.WarmupPlacement = models.WarmupPlacementEvidence{MajorDelivered: 22, MajorSpam: 6}
|
||||
byKey := findingsByKey(Detect(snapshotOf(m), defaults()))
|
||||
if _, ok := byKey["warmup_pool_blocked"]; ok {
|
||||
t.Fatal("a throttled mailbox is still described as excluded from the pool")
|
||||
}
|
||||
f, ok := byKey["warmup_pool_throttled"]
|
||||
if !ok || f.Severity != models.AdvisorHigh || f.Action != nil {
|
||||
t.Fatalf("expected a non-critical slowdown finding without a hold: %+v", f)
|
||||
}
|
||||
if !strings.Contains(f.Detail, reason) || !strings.Contains(f.Remedy, "Warmbly manage") {
|
||||
t.Fatalf("throttle advice does not explain the reason and automatic recovery: %+v", f)
|
||||
}
|
||||
for _, key := range []string{"warmup_pool_throttled", "mailbox_spam_placement"} {
|
||||
f := byKey[key]
|
||||
copy := strings.ToLower(f.Remedy + " " + strings.Join(f.Steps, " "))
|
||||
for _, old := range []string{"stop cold sending", "take the mailbox off", "out of cold rotation for a week", "until it requalifies", "bring it back at 5"} {
|
||||
if strings.Contains(copy, old) {
|
||||
t.Errorf("%s still recommends unnecessary exclusion: %s", key, copy)
|
||||
}
|
||||
}
|
||||
if f.Action != nil || f.Evidence["recommended_action"] != f.Remedy {
|
||||
t.Errorf("%s carries a hold or stale narration guidance", key)
|
||||
}
|
||||
}
|
||||
settings := defaults()
|
||||
settings.MutedDetectors = []string{"warmup_pool_throttled"}
|
||||
if _, ok := findingsByKey(Detect(snapshotOf(m), settings))["warmup_pool_throttled"]; ok {
|
||||
t.Fatal("the slowdown finding ignored its own mute setting")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolExclusionIsSeparateFromRecoverableBands(t *testing.T) {
|
||||
for _, state := range []string{"healthy", "watch", "quarantined", "blocked", ""} {
|
||||
t.Run(state, func(t *testing.T) {
|
||||
m := healthyMailbox()
|
||||
m.PoolHealth, m.PoolBlocked = state, true
|
||||
byKey := findingsByKey(Detect(snapshotOf(m), defaults()))
|
||||
if _, ok := byKey["warmup_pool_throttled"]; ok {
|
||||
t.Fatal("a non-throttled mailbox produced a throttle finding")
|
||||
}
|
||||
f, blocked := byKey["warmup_pool_blocked"]
|
||||
if state == "healthy" || state == "watch" {
|
||||
if blocked {
|
||||
t.Fatalf("%s was treated as pool exclusion solely for its timer", state)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !blocked || f.Severity != models.AdvisorCritical || f.Action == nil || f.Action.Tool != "set_mailbox_send_hold" {
|
||||
t.Fatalf("real pool exclusion lost its severe guidance or hold: %+v", f)
|
||||
}
|
||||
if !strings.Contains(f.Remedy, "Stop cold sending") || f.Action.Undo == nil || strings.Contains(string(f.Action.Args), "status") {
|
||||
t.Fatalf("pool exclusion must offer a reversible campaign hold without disabling warmup: %+v", f)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestThrottleDoesNotSuppressIndependentSafetyFindings(t *testing.T) {
|
||||
m := healthyMailbox()
|
||||
m.PoolHealth = "throttled"
|
||||
m.AuthState, m.AuthSPF, m.AuthDMARC = "failing", false, false
|
||||
m.Complaints30d, m.Bounces30d = 2, 80
|
||||
m.WarmupPlacement = models.WarmupPlacementEvidence{MajorDelivered: 100, MajorSpam: 50}
|
||||
byKey := findingsByKey(Detect(snapshotOf(m), defaults()))
|
||||
for _, key := range []string{"mailbox_domain_auth", "mailbox_complaint_rate", "mailbox_bounce_rate", "mailbox_spam_placement"} {
|
||||
if f, ok := byKey[key]; !ok || f.Severity != models.AdvisorCritical {
|
||||
t.Errorf("throttling suppressed independent safety finding %s: %+v", key, f)
|
||||
}
|
||||
}
|
||||
if f := byKey["mailbox_spam_placement"]; f.Action == nil || f.Action.Tool != "set_mailbox_send_hold" {
|
||||
t.Fatal("severe placement lost its hold action on a throttled mailbox")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatedPlacementGuidanceInvalidatesOldNarration(t *testing.T) {
|
||||
m := healthyMailbox()
|
||||
m.WarmupPlacement = models.WarmupPlacementEvidence{MajorDelivered: 22, MajorSpam: 6}
|
||||
f := findingsByKey(Detect(snapshotOf(m), defaults()))["mailbox_spam_placement"]
|
||||
current := f.toModel(uuid.New())
|
||||
legacy := *current
|
||||
var err error
|
||||
legacy.Evidence, err = json.Marshal(map[string]any{
|
||||
"mailbox": m.Email,
|
||||
"major_provider_spam_7d": 6,
|
||||
"major_provider_delivered_7d": 22,
|
||||
"spam_placement_percent": band(100.0 * 6 / 22),
|
||||
"quarantine_band_percent": 20.0,
|
||||
"currently_sending_cold": m.SendingCold(),
|
||||
"current_daily_cap": m.CampaignLimit,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if current.EvidenceHash() == legacy.EvidenceHash() || cacheKey(current) == cacheKey(&legacy) {
|
||||
t.Fatal("unchanged placement numbers would retain the old stop-sending narration after an upgrade")
|
||||
}
|
||||
}
|
||||
|
||||
// A mailbox already out of rotation gets no hold, so Undo can never release a hold someone else set.
|
||||
func TestHoldFixIsOfferedOnlyWhileSendingCold(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
|
||||
@@ -42,6 +42,12 @@ func warmupDetectors() []Detector {
|
||||
About: "A Cloud-linked mailbox with missing or stale status evidence. New sends are held until a recognized Cloud observation arrives; this is an availability hold, not a reputation ban or a provider authentication failure.",
|
||||
Run: detectCloudStandingUnavailable,
|
||||
},
|
||||
{
|
||||
Key: "warmup_pool_throttled",
|
||||
Category: models.AdvisorCategoryWarmup,
|
||||
About: "A mailbox whose warmup and cold sending are temporarily reduced by its health band. Throttling keeps the mailbox in the pool, including during re-entry probation. Placement throttles recover automatically through watch as placement improves. Throttling is not pool exclusion, a connection failure, or a reason on its own to hold campaigns or require manual re-entry. Follow the recommended action in the evidence.",
|
||||
Run: detectWarmupPoolThrottled,
|
||||
},
|
||||
{
|
||||
Key: "warmup_pool_blocked",
|
||||
Category: models.AdvisorCategoryWarmup,
|
||||
@@ -285,14 +291,60 @@ func detectCloudStandingUnavailable(s *repository.AdvisorSnapshot) []Finding {
|
||||
return out
|
||||
}
|
||||
|
||||
func detectWarmupPoolThrottled(s *repository.AdvisorSnapshot) []Finding {
|
||||
out := []Finding{}
|
||||
for _, m := range s.Mailboxes {
|
||||
if m.PoolHealth != string(models.WarmupHealthThrottled) || m.PoolHealthReason == "cloud_evidence_unavailable" {
|
||||
continue
|
||||
}
|
||||
remedy := "Keep warmup running and let Warmbly manage the reduced sending pace. Do not raise the daily cap to bypass the slowdown. Recovery follows the reason shown, without manual pool re-entry; separate ramp limits still apply."
|
||||
out = append(out, Finding{
|
||||
Key: "warmup_pool_throttled",
|
||||
GroupTitle: "{count} mailboxes have temporarily reduced sending",
|
||||
Category: models.AdvisorCategoryWarmup,
|
||||
Severity: models.AdvisorHigh,
|
||||
Surface: models.AdvisorSurfaceMailboxes,
|
||||
EntityType: "email_account",
|
||||
EntityID: ref(m.ID),
|
||||
EntityLabel: m.Email,
|
||||
Impact: clampImpact(40 + int(m.PoolHealthScore)/4),
|
||||
Title: fmt.Sprintf("%s has temporarily reduced sending", m.Email),
|
||||
Detail: fmt.Sprintf(
|
||||
"Warmbly has reduced warmup and cold sending for %s to half volume with wider spacing. This status describes a reduced sending pace, not pool exclusion or a connection failure. %s",
|
||||
m.Email, m.PoolHealthReason),
|
||||
Remedy: remedy,
|
||||
Steps: []string{
|
||||
"Keep warmup enabled so recent placement evidence can show recovery.",
|
||||
"Leave the daily cap at or below its current setting while Warmbly manages the reduced pace.",
|
||||
"Review recent campaign targeting and copy, and investigate any confirmed authentication, complaint or bounce issues separately.",
|
||||
"For a placement throttle, the half-volume limit steps down to watch below 15% spam placement, and watch lifts below 7.5%. Other cooldowns follow the reason shown. Warmbly re-evaluates these limits automatically.",
|
||||
},
|
||||
Evidence: map[string]any{
|
||||
"mailbox": m.Email,
|
||||
"pool_state": m.PoolHealth,
|
||||
"pool_health_reason": m.PoolHealthReason,
|
||||
"pool_type": m.WarmupPoolType,
|
||||
"currently_sending_cold": m.SendingCold(),
|
||||
"recommended_action": remedy,
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func detectWarmupPoolBlocked(s *repository.AdvisorSnapshot) []Finding {
|
||||
out := []Finding{}
|
||||
for _, m := range s.Mailboxes {
|
||||
if m.PoolHealthReason == "cloud_evidence_unavailable" {
|
||||
continue
|
||||
}
|
||||
// Recoverable bands also carry blocked_until; their timer is not pool exclusion.
|
||||
switch models.WarmupHealthState(m.PoolHealth) {
|
||||
case models.WarmupHealthHealthy, models.WarmupHealthWatch, models.WarmupHealthThrottled:
|
||||
continue
|
||||
}
|
||||
bad := m.PoolBlocked ||
|
||||
m.PoolHealth == "quarantined" || m.PoolHealth == "blocked" || m.PoolHealth == "throttled"
|
||||
m.PoolHealth == "quarantined" || m.PoolHealth == "blocked"
|
||||
if !bad {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ const (
|
||||
|
||||
// Spam-folder placement at Google, Microsoft and Yahoo. The pool only
|
||||
// slows a mailbox at these; the advice is what the member may choose.
|
||||
spamPlacementWarn = 10.0
|
||||
spamPlacementQuarantine = 20.0
|
||||
spamPlacementBlock = 50.0
|
||||
spamPlacementWarn = 10.0
|
||||
spamPlacementSlowdown = 20.0
|
||||
spamPlacementBlock = 50.0
|
||||
|
||||
// --- mailbox volume ----------------------------------------------------
|
||||
// The repo defaults: 50/day cold cap, 600s minimum gap.
|
||||
|
||||
@@ -236,6 +236,13 @@ func TestLiveHealthPlacementRecoveryUsesTheDisplayedUTCWindow(t *testing.T) {
|
||||
if err != nil || old.Placement.MajorDelivered != 39 || old.Placement.MajorSpam != 6 {
|
||||
t.Fatalf("old rolling sample = %+v, err = %v", old.Placement, err)
|
||||
}
|
||||
snapshot, err := repository.NewAdvisorRepository(handle).LoadSnapshot(t.Context(), f.org, now)
|
||||
if err != nil || len(snapshot.Mailboxes) != 1 {
|
||||
t.Fatalf("advisor snapshot = %+v, err = %v", snapshot, err)
|
||||
}
|
||||
if p := snapshot.Mailboxes[0].WarmupPlacement; p.MajorDelivered != 30 || p.MajorSpam != 2 {
|
||||
t.Fatalf("advisor did not use the displayed placement window: %+v", p)
|
||||
}
|
||||
svc := NewService(repo).(*service)
|
||||
svc.now = func() time.Time { return now }
|
||||
health, xerr := svc.evaluateAndPersistAnyPool(t.Context(), f.account)
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
)
|
||||
|
||||
// LoadSnapshot reads one org's advisor view. Every section is best-effort: a
|
||||
@@ -18,7 +20,7 @@ func (r *advisorRepository) LoadSnapshot(ctx context.Context, orgID uuid.UUID, n
|
||||
Lists: map[uuid.UUID]AdvisorListStats{},
|
||||
}
|
||||
|
||||
mailboxes, err := r.loadMailboxes(ctx, orgID)
|
||||
mailboxes, err := r.loadMailboxes(ctx, orgID, now)
|
||||
if err != nil {
|
||||
// Mailboxes are the one section worth failing on: almost every detector
|
||||
// is scoped to a mailbox, and an empty list would look like "org has no
|
||||
@@ -35,7 +37,7 @@ func (r *advisorRepository) LoadSnapshot(ctx context.Context, orgID uuid.UUID, n
|
||||
return snap, nil
|
||||
}
|
||||
|
||||
func (r *advisorRepository) loadMailboxes(ctx context.Context, orgID uuid.UUID) ([]AdvisorMailbox, error) {
|
||||
func (r *advisorRepository) loadMailboxes(ctx context.Context, orgID uuid.UUID, now time.Time) ([]AdvisorMailbox, error) {
|
||||
// Bounce/complaint attribution runs through tasks: deliverability_events
|
||||
// carries task_id, not email_account_id, so a mailbox's complaints are the
|
||||
// events whose task was sent by it.
|
||||
@@ -91,7 +93,7 @@ func (r *advisorRepository) loadMailboxes(ctx context.Context, orgID uuid.UUID)
|
||||
FROM warmup_statistics wst
|
||||
WHERE wst.email_account_id = ea.id AND wst.date > CURRENT_DATE - 7
|
||||
) w ON true
|
||||
LEFT JOIN LATERAL (` + placementEvidenceSQL("ea.id", "NOW() - INTERVAL '7 days'") + `
|
||||
LEFT JOIN LATERAL (` + placementEvidenceSQL("ea.id", "$2") + `
|
||||
) ws ON true
|
||||
LEFT JOIN LATERAL (` + warmupStandingSQL("ea.id") + `
|
||||
) p ON true
|
||||
@@ -126,7 +128,7 @@ func (r *advisorRepository) loadMailboxes(ctx context.Context, orgID uuid.UUID)
|
||||
WHERE ea.organization_id = $1
|
||||
ORDER BY ea.created_at ASC`
|
||||
|
||||
rows, err := r.db.Query(ctx, query, orgID)
|
||||
rows, err := r.db.Query(ctx, query, orgID, models.WarmupPlacementWindowStart(now))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user