Skip to content

feat(analytics): ingest backend item events over gRPC - #165

Merged
vovinacci merged 4 commits into
mainfrom
feat/analytics-ingest
Jul 21, 2026
Merged

feat(analytics): ingest backend item events over gRPC#165
vovinacci merged 4 commits into
mainfrom
feat/analytics-ingest

Conversation

@vovinacci

@vovinacci vovinacci commented Jul 21, 2026

Copy link
Copy Markdown
Owner

The ADR-0002 client loop: dial backend:50051, ListItems snapshot reconcile (upsert current_items, tombstone rows missing from the snapshot), then consume WatchItemEvents; on any failure, equal-jitter exponential backoff (1s base, 30s cap) and a fresh dial + snapshot. Snapshot recovers state, not missed events -- aggregate dips stay visible by design, and reconcile never synthesizes bucket entries.

Each stream event runs one transaction: raw insert with ON CONFLICT DO NOTHING on (item_id, event_type, event_time), an hourly bucket upsert keyed on date_trunc('hour', event_time, 'UTC') -- event time, never arrival time, timezone pinned explicitly -- that increments only when the raw insert actually landed (replays cannot double-count), and an event-time-gated current_items upsert (a late out-of-order event cannot un-tombstone or rename a row the future already wrote). analytics_last_event_time_seconds is a monotonic watermark (CAS max-advance) so the Phase 5 seeder's deliberately late events will not regress staleness panels.

Read API for the canary and reports: GET /api/v1/items/{id} (200 with tombstoned flag when seen, 404 never-seen) and GET /api/v1/stats. AnalyticsStreamDown alert fires only while the process itself is up -- stream loss and service loss stay distinct signals -- with runbook. Ships the analytics ingest dashboard and the break-the-event-stream exercise. Analytics tests now run go test -p 1: store and ingest suites share one throwaway Postgres and raced each other's schema teardown under cross-package parallelism.

Summary by CodeRabbit

  • New Features
    • Analytics now continuously ingests item updates and reconnects automatically if the event stream is interrupted.
    • Added read-only APIs for current item details and 24-hour event statistics.
    • Added analytics monitoring dashboards and alerts for disconnected ingestion streams.
  • Documentation
    • Expanded analytics setup, architecture, API, observability and troubleshooting guidance.
    • Added a hands-on exercise for testing event-stream failures and recovery.
  • Bug Fixes
    • Improved handling of duplicate, delayed and out-of-order events to maintain accurate item state and statistics.
  • Chores
    • Improved analytics test reliability and deployment configuration.

The ADR-0002 client loop: dial backend:50051, ListItems snapshot
reconcile (upsert current_items, tombstone rows missing from the
snapshot), then consume WatchItemEvents; on any failure, equal-jitter
exponential backoff (1s base, 30s cap) and a fresh dial + snapshot.
Snapshot recovers state, not missed events -- aggregate dips stay
visible by design, and reconcile never synthesizes bucket entries.

Each stream event runs one transaction: raw insert with ON CONFLICT
DO NOTHING on (item_id, event_type, event_time), an hourly bucket
upsert keyed on date_trunc('hour', event_time, 'UTC') -- event time,
never arrival time, timezone pinned explicitly -- that increments only
when the raw insert actually landed (replays cannot double-count), and
an event-time-gated current_items upsert (a late out-of-order event
cannot un-tombstone or rename a row the future already wrote).
analytics_last_event_time_seconds is a monotonic watermark (CAS
max-advance) so the Phase 5 seeder's deliberately late events will not
regress staleness panels.

Read API for the canary and reports: GET /api/v1/items/{id} (200 with
tombstoned flag when seen, 404 never-seen) and GET /api/v1/stats.
AnalyticsStreamDown alert fires only while the process itself is up --
stream loss and service loss stay distinct signals -- with runbook.
Ships the analytics ingest dashboard and the break-the-event-stream
exercise. Analytics tests now run go test -p 1: store and ingest
suites share one throwaway Postgres and raced each other's schema
teardown under cross-package parallelism.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@vovinacci, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 80d87930-782a-4a33-ae50-a9ec2d4e0196

📥 Commits

Reviewing files that changed from the base of the PR and between fb5efae and 94ad13a.

⛔ Files ignored due to path filters (1)
  • services/analytics/go.sum is excluded by !**/*.sum
📒 Files selected for processing (10)
  • .github/workflows/analytics.yml
  • Makefile
  • docs/architecture.md
  • docs/prerequisites.md
  • proto/buf.gen.yaml
  • services/analytics/Dockerfile
  • services/analytics/Makefile
  • services/analytics/go.mod
  • services/analytics/internal/ingest/client.go
  • services/analytics/internal/ingest/client_test.go

Walkthrough

Analytics now ingests backend snapshots and events over reconnecting gRPC, persists reconciled item state and event aggregates, exposes read APIs, and adds deployment configuration, metrics, alerts, dashboards, runbooks, exercises, and CI code-generation updates.

Changes

Analytics ingest

Layer / File(s) Summary
Current-item storage and migration
services/analytics/internal/store/*, services/analytics/internal/store/migrations/*
Adds current_items, snapshot reconciliation, idempotent event ingestion, tombstone handling, hourly statistics, item lookup, and migration tests.
Reconnecting gRPC ingest client
services/analytics/internal/ingest/*
Adds snapshot reconciliation, streamed event ingestion, deduplication, reconnect backoff, metrics, trace propagation, configuration, and integration tests.
Read API and service lifecycle
services/analytics/cmd/analytics/main.go, services/analytics/internal/httpserver/*, services/analytics/README.md
Wires ingestion and storage into service startup and shutdown, and adds item and statistics endpoints with tests and documentation.
Deployment and operational observability
deploy/compose/docker-compose.yml, observability/*, docs/*, README.md
Configures the backend gRPC target and documents the ingest architecture, dashboards, alert, runbook, and stream-disruption exercise.
Serialised tests and audit code generation
.github/workflows/analytics.yml, services/analytics/Makefile
Serialises analytics package tests and generates protobuf/gRPC stubs before vulnerability scanning.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Analytics as Analytics ingest client
  participant Backend as Backend ItemService
  participant Store as Analytics store
  participant Metrics as Prometheus metrics
  Analytics->>Backend: ListItems
  Backend-->>Analytics: Snapshot
  Analytics->>Store: ReconcileSnapshot
  Analytics->>Backend: WatchItemEvents
  Backend-->>Analytics: ItemEvent stream
  Analytics->>Store: IngestEvent
  Analytics->>Metrics: Update stream and ingest metrics
Loading

Possibly related PRs

  • vovinacci/devops-demo#161: Adds the backend ItemService and WatchItemEvents gRPC server consumed by this analytics ingest client.

Poem

A bunny hops where snapshots flow,
Reconnecting streams begin to grow.
Buckets count each event just right,
Dashboards twinkle through the night.
“Tombstones saved!” the rabbit sings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: analytics now ingests backend item events over gRPC.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/analytics-ingest

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Test job now runs go test -p 1: the store and ingest test binaries
share one Postgres service container and raced each other's schema
teardown -- the same fix the local Makefile already carries; the
workflow bypassed it by invoking go test directly.

The govulncheck job broke the moment ingest started importing the
generated internal/pb stubs: generate-in-build means they exist only
after codegen, which the action's own checkout never runs. The job now
checks out, generates, and passes repo-checkout: false so the scan
sees the same tree.
@vovinacci

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

protoc-gen-go and protoc-gen-go-grpc move from ad-hoc go install pins
(workflow env vars + Dockerfile args, invisible to Renovate) into
go.mod tool directives: buf.gen.yaml invokes them through go tool, so
their versions live in go.mod/go.sum where Renovate manages them, and
protoc-gen-go is MVS-locked to the protobuf runtime the service
compiles against -- generator/runtime drift becomes structurally
impossible. A new make -C services/analytics generate target is the
single codegen entry point for the root Makefile, CI, and the
Dockerfile docs; CI installs buf via bufbuild/buf-action setup_only,
the same pattern proto.yml already uses.
@vovinacci

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@vovinacci

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@vovinacci

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/analytics.yml:
- Around line 119-151: Add job-level permissions to audit-analytics granting
only contents: read for checkout and code generation. Do not add security-events
or any broader permission scope.

In `@docs/architecture.md`:
- Around line 72-79: Update the Backend component description in
docs/architecture.md to remove the stale statement that no consumer is
connected. Keep the documented analytics consumer and WatchItemEvents topology
consistent with the gRPC ingest client description.

In `@services/analytics/internal/ingest/client.go`:
- Around line 139-143: Update the event-processing loop around handleEvent so
persistence failures from Store.IngestEvent are retried for the same event
before calling Recv again, preserving the stream-connected state and preventing
data loss. Distinguish these transient write failures from validated malformed
payloads, which should remain intentionally logged and discarded; use the
existing handleEvent and persistence error path rather than changing unrelated
stream handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 613fb8a8-eeed-4013-bdfd-bb9c8ec93da8

📥 Commits

Reviewing files that changed from the base of the PR and between aaadc19 and fb5efae.

📒 Files selected for processing (23)
  • .github/workflows/analytics.yml
  • README.md
  • deploy/compose/docker-compose.yml
  • docs/architecture.md
  • docs/exercises/03-break-the-event-stream.md
  • docs/runbooks/analytics-stream-down.md
  • observability/grafana/dashboards/analytics.json
  • observability/prometheus_alerts.yml
  • services/analytics/Makefile
  • services/analytics/README.md
  • services/analytics/cmd/analytics/main.go
  • services/analytics/go.mod
  • services/analytics/internal/httpserver/server.go
  • services/analytics/internal/httpserver/server_test.go
  • services/analytics/internal/ingest/client.go
  • services/analytics/internal/ingest/client_test.go
  • services/analytics/internal/ingest/config.go
  • services/analytics/internal/ingest/metrics.go
  • services/analytics/internal/ingest/trace.go
  • services/analytics/internal/store/current_items.go
  • services/analytics/internal/store/current_items_test.go
  • services/analytics/internal/store/migrate_test.go
  • services/analytics/internal/store/migrations/0002_current_items.sql

Comment thread .github/workflows/analytics.yml
Comment thread docs/architecture.md
Comment thread services/analytics/internal/ingest/client.go Outdated
Review findings: a DB blip during stream consumption silently dropped
the event while the stream stayed healthy -- the loop now retries the
same event (1s pace) before the next Recv, and only validated-malformed
payloads (unknown enum, marked with a sentinel error) are logged and
discarded. A sustained outage self-resolves through the backend's
bounded per-subscriber queue: it disconnects this client and the
normal reconnect+snapshot path takes over. Regression test drives a
flaky store (two failures, then success) plus a malformed event
through a live stream.

Also: audit-analytics job gets least-privilege contents: read, and
docs/architecture.md drops the stale 'no consumer connected yet'
backend note now that the ingest client exists.
@github-actions

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Proto / breaking-proto (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped⏩ skipped✅ passedJul 21, 2026, 9:25 PM

@github-actions

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Proto / lint-proto (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed⏩ skippedJul 21, 2026, 9:25 PM

@vovinacci
vovinacci merged commit eb47deb into main Jul 21, 2026
14 checks passed
@vovinacci
vovinacci deleted the feat/analytics-ingest branch July 21, 2026 21:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant