Repository navigation
Honor the first email delay in sequences - #168
Merged
Merged
Conversation
- Schedule sequence enrollment from the first published email’s delay - Clarify sequence and broadcast delay semantics in MCP and docs
Adds a multi-day journey test that drives enroll -> poll -> send with a faked clock, so a first email that ignores its delay fails a test. Before sending a sequence email, check that the contact's entry time plus the delays so far has passed; if not, log a warning and send `sequence_email_sent_early` to PostHog. Report only: the email still goes out. Skipped when an email involved changed after the contact entered. Enrollment now stamps created_at from the app clock the check uses.
A sequence that has sent nothing can still have contacts waiting for a delayed email, which "Sent" alone hid. - Stats gain a Waiting figure; each email row shows "N waiting". - Clicking the count opens a dialog listing the waiting contacts with their own due times, soonest first, with search by email or name and cursor paging. - API: GET /sequences/:id/schedule and GET /sequences/:id/emails/:emailId/waiting (q, after, limit; 400 invalid_cursor). MCP: get_sequence_schedule and list_sequence_email_waiting_contacts. - Index ongoing_sequences (sequence_id, next_email_scheduled_time, id) so a sequence's list doesn't walk other sequences' rows (427 ms -> 0.4 ms worst case at 100k waiting). - SequenceEmailList gains renderEmailDetail. - Docs for sequences, MCP and the component.
- Early-send check: only check a sequence's first email, after a successful send, and skip it when the email or sequence changed after the contact entered. Summing every delay since entry misfired for contacts enrolled before the delay fix and after reorders or deletes. Adding or deleting an email now bumps sequences.updated_at. - Search treats %, _ and \ literally; contacts search shares the condition (contactSearchCondition). - Dialog: delays read "30 minutes" / "2 hours" rather than "0.0 days"; an early Next is no longer undone by the search debounce. - Sequence page refetches the schedule when the sequence changes, so "N waiting" follows reorders, publishes and deletes. - MCP output schemas come from @sendlit/api-contract; drop the getNextPublishedEmail re-export.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A sequence whose only email was set to 13 days showed "Sent 3" the day after it was activated. Enrollment set the first send to "now", and the first email's delay was never read: only follow-up emails waited their delay.
Fix (
e1e09a1)delayInMillisdescription say where delays count from.Guardrails (
d5948e5)setSystemTime). It fails if the fix is reverted.sequence_email_sent_early(sequence_id,email_id,ongoing_sequence_id,early_by_ms) to PostHog. Later emails aren't checked, because reordering or deleting emails legitimately changes their timing. Skipped when the email or the sequence changed after the contact entered, which also covers contacts enrolled before this fix.Visibility (
b45c23d)GET /sequences/:sequenceId/scheduleandGET /sequences/:sequenceId/emails/:emailId/waiting(q,after,limit; a cursor that can't be read returns 400invalid_cursor).get_sequence_schedule,list_sequence_email_waiting_contacts.SequenceEmailListgainsrenderEmailDetail.Review fixes (
4e9c782)updated_at.%,_and\literally, on the contacts page too.Migration
0030_waiting_contacts_indexaddsongoing_sequences (sequence_id, next_email_scheduled_time, id). Without it, a sequence's list walks every other sequence's rows in the global due-time index.Drizzle runs migrations in a transaction, so the build blocks writes to
ongoing_sequenceswhile it runs: about 1 s for 500k rows, and instant at today's size. The migration usesIF NOT EXISTS(0e1d31b), so if the table ever gets large enough to matter, a future index like this can be built by hand withCONCURRENTLYfirst.Testing
bun test --parallel src(api): 959 of 960 pass. The one failure is the pacing test "(Scope new-account pacing to the risk: don't throttle a customer's own ESP by account age #122)", which times out under parallel load and does the same onmain.bun run test(web): 111 pass.test:conformanceandcheck:driftpass.Follow-up
sequence_email_sent_early.