6.6 KiB
Add @suaveplan/delivery-tracking
Package: @suaveplan/delivery-tracking
Status: PROPOSED
Type: new-package
Design: ./design.md
Spec delta: ./specs/delivery-tracking/spec.md
Why
Suaveplan ships a multi-channel campaign + transactional message pipeline. Without a channel-agnostic delivery event tracking surface, every channel (emailer, notification-channel, future SMS, push, in-app) has to invent its own way to capture sends/opens/clicks/bounces and roll them up for the dashboard. The upstream @suaveplan/delivery-tracking package solves that with a typed tracker, pluggable store, webhook parsers for the major email providers, and pixel/link generators. Genesis needs the same surface to unblock downstream consumers and to feed the OpenTelemetry-based analytics already wired up in the API host.
What changes
This is a brand-new package on genesis. Public surface:
- ADDED:
DeliveryTracker— Zod-validated write surface with lifecycle dedupe, typedEventBusemission, injectableClock/IdGenerator, OTel counters. - ADDED:
DeliveryAggregator— campaign/contact rollups + time-series bucketing. - ADDED:
DeliveryEventStoreinterface +InMemoryDeliveryEventStore+SurrealDeliveryEventStore(positional SurQL). - ADDED:
TrackingPixelGenerator+TrackingLinkWrapper+ sharedencodeToken/decodeTokencodec. - ADDED:
WebhookEventParserwithsendgrid/mailgun/ses/genericbuilt-ins and runtimeregisterProvider. - ADDED:
DELIVERY_TRACKING_CODES+DeliveryTrackingError(5 codes:INVALID_INPUT,INVALID_TOKEN,UNSUPPORTED_PROVIDER,STORE_FAILURE,QUERY_FAILURE). - ADDED:
getDeliveryTrackingInstruments/resetDeliveryTrackingInstrumentsOTel counter set. - ADDED: Domain types —
DeliveryEvent,DeliveryEventType,DeliveryStats,CreateDeliveryEventInput,DeliveryEventQuery,TrackingConfig,TrackingTokenPayload,ClickTrackingTokenPayload,TimeSeriesBucket,DeliveryTrackerEventMap.
Out of scope
- HTTP route handlers for pixel/click redirect and webhook ingest. The package exposes the parser + tracker; mounting them into a Hono / api-plugin is the consumer's job.
- HMAC-signed tokens. The shared codec is XOR + base64url for URL compactness; consumers needing tamper resistance layer their own HMAC.
- Multi-tenant access control over reads. The aggregator returns whatever the store returns; authorization belongs to the API layer above.
Source mapping
This change derives from ../suaveplan/enterprise/delivery-tracking.md.
| Requirement | Source lines | Notes |
|---|---|---|
| Channel-agnostic delivery event tracking | L11-L13 | verbatim (purpose statement) |
| Zero browser / Node / React deps; Bun + Node compatible | L37 | verbatim — package is server-tier (uses Buffer) but no React/DOM dependency |
| Zod schemas for public types | L38 | enforced via createDeliveryEventInputSchema |
| Deterministic behaviour — injectable Clock + RNG | L40 | Clock, IdGenerator from @suaveplan/types are injectable |
| OTel instrumentation via shared adapter | L41 | getDeliveryTrackingInstruments() lazy-initialised counter set |
Structured errors extending @suaveplan/error |
L42 | DeliveryTrackingError + defineCodes |
| 100% test coverage (Istanbul) | L50 | enforced in vitest.config.ts thresholds |
100% docs coverage; sibling .md per impl file |
L51 | every .ts outside the §4-exempt set has a sibling .md ≥200 words |
| File-size budget 250 soft / 300 hard / 500 test | L52 | every file inside budget |
| No mocks — real services / real OTel exporter | L53 | InMemoryMetricExporter from @opentelemetry/sdk-metrics |
sideEffects: false + workspace internal deps |
L55-L56 | verified in package.json |
| Public API stability via Zod schemas | L60 | createDeliveryEventInputSchema is the boundary contract |
Structured logging support via @suaveplan/logger |
L62 | consumed via LoggerLike from @suaveplan/types (caller-injected) |
| Plugin / extension interface | L69 | WebhookEventParser accepts custom ProviderParser registrations |
| Observability surface — counters + histograms | L70 | five named counters cover ingest, dedupe, webhook, token issuance, token rejection |
| Configurability — Zod-validated options w/ defaults | L71 | DeliveryTrackerOptions, TrackingConfig documented per-field |
| Resilience — typed errors on every IO path | L73 | every adapter exception wrapped in STORE_FAILURE/QUERY_FAILURE |
| Security — input validation at the boundary | L74 | every tracker write parses through Zod first |
Requirements with no source line
| Requirement | Justification |
|---|---|
Genesis runner-neutral test imports (@suaveplan/testing/runner) |
Genesis-wide policy (CLAUDE.md rule 11), not package-specific. |
| Positional SurQL for SurrealDB store | Genesis convention established by api-plugin-events to keep query bodies auditable; upstream already followed this pattern. |
Genesis 0.1.0 start version |
Genesis lifecycle rule (CLAUDE.md). |
Verification approach
Every requirement in the spec delta names concrete it(...) titles via **Verified by:** lines. The packaged test surface:
src/errors/delivery-tracking-error.test.ts— registry + class behaviour.src/otel/instruments.test.ts— lazy construction + reset.src/stores/memory/memory-store.test.ts— interface contract,computeStats, filter/paging behaviour.src/stores/surreal/surreal-store.test.ts— positional SurQL bodies + error wrapping. Stubs theSurrealAdapter.queryboundary because the surface is intentionally a thin SurQL emitter; a full testcontainer integration test belongs totesting-integration-surrealdbconsumers per CLAUDE.md.src/core/tracker/tracker.test.ts— Zod boundary, lifecycle dedupe, bus emission, batch ingest, realInMemoryMetricExportercounter assertions.src/core/aggregator/aggregator.test.ts— rollups + time-series bucketing.src/core/tracking-pixel/token-codec.test.tsandsrc/core/tracking-pixel/tracking-pixel.test.ts— token round-trip + URL/HTML formatting.src/core/tracking-link/tracking-link.test.ts— single + bulk wrap + parse.src/core/webhook-parser/webhook-parser.test.ts— every built-in provider + extensibility + unsupported error.
Impact
Affected dependents
No dependents on genesis yet — this is a fresh landing. Run bunx turbo ls --filter=...@suaveplan/delivery-tracking to enumerate once the package is published. The notification-channel and emailer migrations being executed concurrently will consume this surface; their proposals own the wire-in step.
Migration impact
None — additive only.