Files
2026-05-27 23:29:53 +01:00

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, typed EventBus emission, injectable Clock/IdGenerator, OTel counters.
  • ADDED: DeliveryAggregator — campaign/contact rollups + time-series bucketing.
  • ADDED: DeliveryEventStore interface + InMemoryDeliveryEventStore + SurrealDeliveryEventStore (positional SurQL).
  • ADDED: TrackingPixelGenerator + TrackingLinkWrapper + shared encodeToken/decodeToken codec.
  • ADDED: WebhookEventParser with sendgrid/mailgun/ses/generic built-ins and runtime registerProvider.
  • ADDED: DELIVERY_TRACKING_CODES + DeliveryTrackingError (5 codes: INVALID_INPUT, INVALID_TOKEN, UNSUPPORTED_PROVIDER, STORE_FAILURE, QUERY_FAILURE).
  • ADDED: getDeliveryTrackingInstruments/resetDeliveryTrackingInstruments OTel 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 the SurrealAdapter.query boundary because the surface is intentionally a thin SurQL emitter; a full testcontainer integration test belongs to testing-integration-surrealdb consumers per CLAUDE.md.
  • src/core/tracker/tracker.test.ts — Zod boundary, lifecycle dedupe, bus emission, batch ingest, real InMemoryMetricExporter counter assertions.
  • src/core/aggregator/aggregator.test.ts — rollups + time-series bucketing.
  • src/core/tracking-pixel/token-codec.test.ts and src/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.