Sprint 5: stats endpoint + data integrity — transactional ingestion, impression beacon, registered event types - #16
Merged
Merged
Conversation
Scope decided with Steve: GET /v1/stats (sk-only, totals + per-entity), registered event types with typo rejection, transactional ingestion (#2), SQL-side progress increments (#3), impression beacon (#6), config-plane zod validation (#4), SDK secretKey + getStats(), demo stats page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ten tasks in dependency order: contracts → core (delta evaluate, suggester, new ports) → adapter-db (transactional ingestion, stats store, migration 0003) → cms (registered event types) → adapter-strapi (zod validation, #4) → api (ingestion rewire #2/#3, impression beacon #6, stats route) → sdk + widgets → demo stats page + e2e + docs DoD. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… schemas Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ion and stats ports Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ore, impression dedup migration Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ered event types (closes #4 scope) Replaces every unchecked `as` cast / blind String()/Number()/Boolean() coercion on StrapiConfigPlane's fetch responses with module-local zod schemas (packages/adapter-strapi/src/schemas.ts), so a malformed CMS response is treated exactly like a failed fetch and falls through to the existing TTL stale-on-error path. verifyKey validates environment/keyType as strict enums and fails closed to null auth (never a corrupt AuthContext) on any parse failure. Adds getRegisteredEventTypes(projectId), implementing the ConfigStore port widened in an earlier task, against the config-plane event-types endpoint with the same TTL cache + stale-on-error machinery as the other methods. Drops the unnecessary content-type header on GET requests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on (closes #2, #3 scope) Rewires POST /v1/events onto the transactional IngestionStore (dedup, clamped progress, exactly-once unlocks in one DB transaction) and rejects unregistered event types with a typo suggestion, failing open if the config plane is unreachable. Also fixes a placements.ts compile break (unrelated 5-arg OfferMetricsStore widening from an earlier task) so the workspace typechecks green again. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ger records (closes #6 scope) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ssion beacon Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…de try Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds apps/demo/app/stats/page.tsx, a server component that calls the secret-key-only GET /v1/stats via @promocean/sdk and renders totals plus achievement/offer/timed-event tables. Seeds a demo sk key alongside the existing pk key (apps/cms/src/index.ts), wires PROMOCEAN_SECRET_KEY through .env.example and CI. Extends offer-loop.spec.ts to assert the impression beacon fires exactly once on render and never for a dismissed offer, and achievement-loop.spec.ts to correlate a real unlock with its /stats row. Documents getStats/secretKey and recordImpression in the SDK README, impression semantics in the widgets README, and the new API surface rows + registered-event-types opt-in in the root README. closes #2, #3, #6; closes the #4 scope (adapter validation + cache tests). Registered event types and stats are the first two v1.x roadmap items delivered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ationale Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Implements the Sprint 5 plan (
docs/superpowers/plans/2026-07-07-sprint-5-stats-integrity.md) — the first v1.x sprint, themed "trustworthy numbers": fix the write path, then build the stats read path on top.IngestionStorewraps event dedup-insert → progress increments → unlock inserts → usage in onedb.transaction; a mid-chain failure rolls back the event row too, so dedup now records completion and SDK retries re-process instead of silently skippingLEAST(current + delta, target) … RETURNING current);evaluateEventreturns deltas and the post-increment value decides unlocks — concurrent events can no longer lose updates (Testcontainers race + rollback tests)Placementwidget fires an idempotentPOST /v1/offers/:id/impressionbeacon only after the render decision passes the dismissal check (partial unique index dedups retries);resolveOffergains a deterministic equal-priority tie-breakverifyKeyfails closed on bad enums with a warn log; the three missing cache-path tests added400 unregistered_event_typewith a Levenshtein did-you-mean suggestionGET /v1/stats(secret-key-only): totals + per-achievement/offer (CTR)/timed-event participation, optional from/to range; SDK gains a server-side-onlysecretKeyoption +getStats(); demo gets a server-rendered/statspage (sk never reaches the browser)Behavior note for existing clients
One intentional wire-content change on
POST /v1/events: theprogressarray now includes achievements already at target (clampedcurrent = target) where it previously omitted them. Schema-compatible; spec-intended (evaluation no longer skips — SQL clamping makes at-target increments no-ops).Test plan
/statspage correlating the actual unlocked achievementpnpm turbo run typecheck build testgreen (27/27, re-verified post fix-wave)Review notes
Ten tasks through the full implement→review gate cycle plus a final whole-branch review — verdict ready to merge with fixes, fix wave applied and re-approved (
e68b73f: verify-key validation warn + fail-closed comment, delta rounding rationale). Follow-ups filed: #13 (offer-id validation on impression/click — stats pollution hardening), #14 (test-hardening bundle), #15 (stats polish bundle). Migration0003is safe on existing data (partial unique index predicate excludes all pre-existing rows).🤖 Generated with Claude Code