6.1 KiB
OpenSpec Change: enterprise-campaigns
- Change ID: enterprise-campaigns
- Package:
@suaveplan/campaigns - Category: universal
- Target path:
universal/email/campaigns - Status: IN PROGRESS
- Date: 2026-04-24
- Reference spec:
/mnt/s/web/suaveplan/enterprise/campaigns.md
Summary
Deliver @suaveplan/campaigns on the genesis branch from scratch at enterprise quality. The package provides email campaign orchestration: campaign lifecycle state machine (draft → scheduled → sending → sent / cancelled / failed), audience resolution via @suaveplan/audience-rules, template selection via @suaveplan/email-templates, per-recipient variable injection, send-rate throttling with configurable concurrency, schedule management (immediate, future ISO-8601, recurring via cron expression), campaign analytics aggregation, and A/B variant support. Genesis adds structured errors, Zod schemas, OTel instrumentation, AbortSignal cancellation, idempotent send via @suaveplan/idempotency, and retry with circuit breaker via @suaveplan/retry.
Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build.
Available workspace deps on genesis
| Package | Role |
|---|---|
@suaveplan/error (workspace:^) |
CampaignError; CAMPAIGN.* error codes |
@suaveplan/types (workspace:^) |
LoggerLike, Clock, EventBusLike |
@suaveplan/events (workspace:^) |
Campaign lifecycle event bus |
@suaveplan/email-templates (workspace:^) |
Template rendering per recipient |
@suaveplan/audience-rules (workspace:^) |
Audience segment resolution |
@suaveplan/idempotency (workspace:^) |
Per-recipient idempotency key guard |
@suaveplan/retry (workspace:^) |
Retry + circuit breaker for delivery calls |
@suaveplan/suppression (workspace:^) |
Suppression list check before send |
zod (catalog:forms) |
Option schema validation |
@opentelemetry/api (catalog:observability) |
Spans + counters + histograms |
Dev deps: vitest, @vitest/coverage-istanbul, fast-check (all catalog:testing).
Public API surface
Core types
type CampaignStatus =
| 'draft' | 'scheduled' | 'sending' | 'sent' | 'cancelled' | 'failed'
interface Campaign {
id: string
name: string
templateName: string
audienceRuleId: string
schedule: CampaignSchedule
status: CampaignStatus
abVariants?: AbVariant[]
metadata: Record<string, unknown>
createdAt: Date
updatedAt: Date
}
interface CampaignSchedule {
type: 'immediate' | 'at' | 'recurring'
at?: string // ISO-8601 for type:'at'
cron?: string // cron expression for type:'recurring'
timezone?: string
}
interface AbVariant {
name: string
templateName: string
fraction: number // 0–1, fractions must sum to ≤1
}
interface CampaignSendOptions {
concurrency?: number // max parallel sends; default 10
ratePerSecond?: number // max sends/second; default 100
signal?: AbortSignal
dryRun?: boolean
}
interface CampaignAnalytics {
campaignId: string
sent: number
failed: number
suppressed: number
durationMs: number
}
CampaignManager
class CampaignManager {
constructor(options: CampaignManagerOptions)
create(draft: Omit<Campaign, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Campaign
schedule(id: string, schedule: CampaignSchedule): Campaign
send(id: string, options?: CampaignSendOptions): Promise<CampaignAnalytics>
cancel(id: string): Campaign
get(id: string): Campaign | undefined
list(filter?: CampaignFilter): Campaign[]
}
interface CampaignManagerOptions {
sender: CampaignSender // injectable delivery adapter
clock?: Clock
logger?: LoggerLike
bus?: EventBusLike
}
interface CampaignSender {
send(recipient: string, subject: string, html: string, text: string): Promise<void>
}
Error codes
type CampaignErrorCode =
| 'CAMPAIGN_NOT_FOUND'
| 'CAMPAIGN_INVALID_TRANSITION'
| 'CAMPAIGN_SEND_FAILED'
| 'CAMPAIGN_TEMPLATE_NOT_FOUND'
| 'CAMPAIGN_AUDIENCE_FAILED'
| 'CAMPAIGN_ABORTED'
Genesis additions
CampaignErrorextendsSuaveplanErrorwith six stable codes.- Zod schemas for
Campaign,CampaignSchedule,AbVariant,CampaignSendOptions,CampaignAnalytics. - A/B variant support — variants are bucketed deterministically by recipient hash (FNV-1a) against configured fractions.
- Suppression list integration — calls
@suaveplan/suppressionbefore every send; suppressed recipients are counted in analytics without being sent to. - OTel instrumentation — span
campaign.send, countercampaign.send_total, histogramcampaign.send_duration_ms, attributescampaign.id,campaign.status,campaign.recipient_count. - Property-based tests — A/B bucketing distribution, state machine transition invariants.
File layout
universal/email/campaigns/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts
types/
common.ts
common.md
core/
campaign-manager/
campaign-manager.ts
campaign-manager.md
campaign-manager.types.ts
campaign-manager.test.ts
index.ts
campaign-fsm/
campaign-fsm.ts
campaign-fsm.md
campaign-fsm.test.ts
index.ts
ab-bucketing/
ab-bucketing.ts
ab-bucketing.md
ab-bucketing.test.ts
index.ts
scheduling/
schedule-parser/
schedule-parser.ts
schedule-parser.md
schedule-parser.test.ts
index.ts
analytics/
campaign-analytics/
campaign-analytics.ts
campaign-analytics.md
campaign-analytics.test.ts
index.ts
__benchmarks__/
campaigns.bench.ts
tests/
integration.test.ts
property.test.ts
Impact
Downstream consumers: any server-side campaign dispatch service.
Workspace dep prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events, @suaveplan/email-templates, @suaveplan/audience-rules, @suaveplan/idempotency, @suaveplan/retry, @suaveplan/suppression.