Skip to content

Feat/click event fanout - #213

Merged
Zingzy merged 21 commits into
mainfrom
feat/click-event-fanout
Jul 3, 2026
Merged

Feat/click event fanout#213
Zingzy merged 21 commits into
mainfrom
feat/click-event-fanout

Conversation

@Zingzy

@Zingzy Zingzy commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary by Sourcery

Introduce a click event fanout pipeline that emits immutable ClickEvent objects from redirect routes into a Redis Stream, processes them asynchronously via a dedicated click worker, and falls back to inline tracking when the stream infrastructure is unavailable or misconfigured.

New Features:

  • Add a ClickEvent DTO and event sink abstraction so redirect routes emit structured click events instead of calling the click service directly.
  • Introduce a Redis Stream–backed click sink and a FastStream-based click worker that consumes click events for stats and hot-URL detection, with configurable consumer groups and dead-lettering.
  • Add optional hotness detection for high-traffic short URLs powered by Redis windowed counters and pluggable actions.
  • Expose ClickEventsSettings in application config to control sink mode, stream/Redis settings, and worker behavior, with safe inline defaults.
  • Wire a dedicated queue Redis and click worker services into local and production Docker configurations, gated behind opt-in env and compose profiles.

Enhancements:

  • Refactor redirect routes to snapshot resolved URL data into ClickEvents, strip sensitive password hashes, and perform pre-emit bot blocking for legacy schemas while keeping v2 bot handling in the pipeline.
  • Centralize ClickService construction for reuse between the web app and the click worker to ensure schema handler parity.
  • Improve Redis client creation with richer connection options, labeled logging, and support for multiple Redis roles (cache vs click queue).

Build:

  • Add FastStream (Redis) to project dependencies to support the click worker process.

Deployment:

  • Extend docker-compose (dev and prod) with a dedicated queue Redis and click-worker service, including health checks, memory policies, and profiles for opt-in deployment of the click event pipeline.

Tests:

  • Expand integration tests around redirect and click tracking to assert on emitted ClickEvents, bot-block behavior, HEAD/password edge cases, and resilience to sink errors.
  • Add unit tests for click event encoding/decoding, click sinks, stats and hotness consumers, DLQ behavior, click events settings, and click worker wiring to validate the new pipeline end-to-end.

Summary by CodeRabbit

  • New Features

    • Added optional click-event streaming support for redirects, including background processing and Redis-backed queueing.
    • Introduced a worker that processes click analytics and hot-traffic detection in the background.
    • Added health and telemetry checks for the new click-processing pipeline.
  • Bug Fixes

    • Improved redirect behavior for bot traffic, password-protected links, and HEAD requests.
    • Ensured click tracking failures do not interrupt normal redirects in most cases.
    • Improved cleanup and fallback behavior when Redis connections or queue processing are unavailable.

Copilot AI review requested due to automatic review settings July 3, 2026 16:49
@sourcery-ai

sourcery-ai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Introduces a click-event fanout architecture: redirect routes now emit immutable ClickEvent objects to an abstract ClickEventSink, which can either process clicks inline (default) or enqueue them to a Redis Stream consumed by a new FastStream-based click worker with stats and hotness consumer groups; configuration, Docker wiring, Redis clients, and tests are updated accordingly while preserving existing redirect and analytics behavior by default.

Sequence diagram for redirect handling with pluggable ClickEventSink

sequenceDiagram
    actor User
    participant App as redirect_url
    participant Sink as ClickEventSink
    participant RedisQ as QueueRedis
    participant Worker as StatsClickConsumer

    User->>App: HTTP GET /{short_code}
    App->>App: resolve URL, auth checks
    App->>App: [bot_precheck for v1/emoji]
    alt bot blocked
        App-->>User: 403 ACCESS DENIED
    else allowed
        App->>App: build ClickEvent
        alt CLICK_EVENTS_SINK=inline or fallback
            App->>Sink: emit(event)
            Sink->>Sink: InlineSink.emit
            Sink->>App: track_click via ClickService
        else CLICK_EVENTS_SINK=stream
            App->>Sink: emit(event)
            Sink->>RedisQ: XADD events:clicks
        end
        App-->>User: HTTP redirect 301/302
    end

    Note over RedisQ,Worker: Asynchronously, worker consumes stream
    RedisQ-->>Worker: XREADGROUP click.recorded
    Worker->>Worker: consume(payload)
    Worker->>App: track_click via ClickService (same logic)
Loading

File-Level Changes

Change Details Files
Redirect routes emit ClickEvent objects into an abstract ClickEventSink instead of calling ClickService directly, with updated bot-blocking and password-handling logic.
  • Replace ClickSvc dependency in redirect routes with ClickSink (ClickEventSink) and inject get_click_sink in dependencies.
  • Construct ClickEvent in redirect_url, including URL snapshot, headers, client IP, redirect_ms, and stripping password_hash, then pass it to click_sink.emit.
  • Introduce pre-emit bot decision in redirect route for v1/emoji URLs using is_bot_request, while preserving analytics-skip semantics for v2 bots and maintaining ForbiddenError-based defense-in-depth in the inline pipeline.
  • Adjust click handlers and ClickService to use redirect_ms from ClickContext instead of recomputing from start_time.
routes/redirect_routes.py
dependencies/services.py
dependencies/__init__.py
services/click/service.py
services/click/protocol.py
services/click/handlers.py
Add ClickEvent DTO, inline and Redis Stream sinks, and a FastStream-based click worker that consumes click events for stats and hotness detection with DLQ handling.
  • Define ClickEvent model and JSON wire format helpers (to_stream_fields, from_stream_fields, click_event_from_payload) for Redis Stream entries.
  • Introduce InlineSink and RedisStreamSink implementations of ClickEventSink, with RedisStreamSink falling back to inline on XADD failures and preserving redirect error semantics.
  • Implement StatsClickConsumer that replays ClickEvent through ClickService with clear failure semantics (drop vs retry), and HotUrlDetector that maintains per-window counters in Redis and invokes HotUrlAction implementations such as LogHotUrlAction.
  • Add click_worker FastStream app that reads from a Redis Stream using reader/claimer subscribers, uses ClaimDeadLetterGuard for DLQing poison messages, wires Mongo/Redis/GeoIP/ClickService, and exposes a /health endpoint.
  • Introduce workers.dlq.ClaimDeadLetterGuard to dead-letter over-delivered messages to a DLQ stream instead of looping indefinitely.
services/click/events.py
services/click/sinks/protocol.py
services/click/sinks/inline.py
services/click/sinks/stream.py
services/click/sinks/__init__.py
services/click/consumers/stats.py
services/click/consumers/hotness.py
services/click/consumers/__init__.py
workers/click_worker.py
workers/dlq.py
Configure click event pipeline via ClickEventsSettings and wire sinks and worker groups, including optional queue Redis, while keeping defaults fully inline and safe.
  • Add ClickEventsSettings to config with env-prefix CLICK_EVENTS_, sink mode (inline
stream), queue_redis_uri, stream/dlq names, batch/claim/delivery tunables, worker_groups, and hotness thresholds, plus validation for known worker groups.
  • Extend AppSettings to include click_events and instantiate it in the model validator so settings.click_events is always present.
  • In app lifespan, create an optional queue_redis (click-events-queue) via create_redis_client only when stream mode and queue_redis_uri are set, and close it on shutdown.
  • In dependencies.wiring, add build_click_service helper to centralize schema→handler mapping, then construct InlineSink and RedisStreamSink based on click_events.sink and queue_redis presence, logging and falling back to inline when stream mode is misconfigured.
  • Update tests to cover ClickEventsSettings behavior and click sink wiring for inline vs stream modes.
  • Enhance Redis client creation to support multiple labeled instances and better connection options, and add queue Redis plus click worker services to Docker configurations.
    • Update create_redis_client to accept a label argument, configure encoding/keepalive/health_check_interval, and log with masked URIs and labels for both cache and queue Redis instances.
    • Add redis-queue and click-worker services to docker-compose.yml with a click-events profile, wiring env vars for CLICK_EVENTS_SINK/CLICK_EVENTS_QUEUE_REDIS_URI and hotness, plus healthchecks.
    • Add redis-queue and click-worker services to docker-compose.prod.yml with noeviction/AOF Redis config, memory limits, and click-worker healthcheck hitting /health; define redis-queue-data volume.
    • Ensure queue Redis uses separate storage and semantics from cache Redis (noeviction vs allkeys-lru), aligning with ClickEventsSettings docs.
    infrastructure/cache/redis_client.py
    docker-compose.yml
    docker-compose.prod.yml
    Update integration and unit tests to exercise the new sink-based click tracking, ClickEvent payloads, worker consumers, DLQ, and bot-block semantics while preserving previous redirect behaviors.
    • Refactor integration tests (redirect_routes, click_tracking, redirect) to inject get_click_sink instead of get_click_service, use _mock_click_sink/_build_app helpers, and assert on ClickEvent contents and sink.emit behavior, including password_hash stripping, HEAD handling, and bot-block scenarios.
    • Add unit tests for ClickEvent serialization, ClickEventsSettings, InlineSink and RedisStreamSink fallback semantics, StatsClickConsumer, HotUrlDetector, click worker app wiring, and ClaimDeadLetterGuard behavior.
    • Adjust existing click service tests to reflect redirect_ms in ClickContext instead of start_time and ensure handler logic still computes metrics correctly.
    • Ensure tests/conftest wires app.state.click_sink for the test app lifecycle so dependencies resolve correctly.
    tests/integration/test_redirect_routes.py
    tests/integration/test_click_tracking.py
    tests/integration/test_redirect.py
    tests/unit/services/test_click_service.py
    tests/unit/services/test_click_events.py
    tests/unit/services/test_click_sinks.py
    tests/unit/services/test_click_consumers.py
    tests/unit/workers/test_click_worker_app.py
    tests/unit/workers/test_dlq_guard.py
    tests/smoke/test_click_sink_wiring.py
    tests/unit/test_click_events_settings.py
    tests/conftest.py

    Tips and commands

    Interacting with Sourcery

    • Trigger a new review: Comment @sourcery-ai review on the pull request.
    • Continue discussions: Reply directly to Sourcery's review comments.
    • Generate a GitHub issue from a review comment: Ask Sourcery to create an
      issue from a review comment by replying to it. You can also reply to a
      review comment with @sourcery-ai issue to create an issue from it.
    • Generate a pull request title: Write @sourcery-ai anywhere in the pull
      request title to generate a title at any time. You can also comment
      @sourcery-ai title on the pull request to (re-)generate the title at any time.
    • Generate a pull request summary: Write @sourcery-ai summary anywhere in
      the pull request body to generate a PR summary at any time exactly where you
      want it. You can also comment @sourcery-ai summary on the pull request to
      (re-)generate the summary at any time.
    • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
      request to (re-)generate the reviewer's guide at any time.
    • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
      pull request to resolve all Sourcery comments. Useful if you've already
      addressed all the comments and don't want to see them anymore.
    • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
      request to dismiss all existing Sourcery reviews. Especially useful if you
      want to start fresh with a new review - don't forget to comment
      @sourcery-ai review to trigger a new review!

    Customizing Your Experience

    Access your dashboard to:

    • Enable or disable review features such as the Sourcery-generated pull request
      summary, the reviewer's guide, and others.
    • Change the review language.
    • Add, remove or edit custom review instructions.
    • Adjust other review settings.

    Getting Help

    @Zingzy Zingzy self-assigned this Jul 3, 2026
    @coderabbitai

    coderabbitai Bot commented Jul 3, 2026

    Copy link
    Copy Markdown

    Review Change Stack

    📝 Walkthrough

    Walkthrough

    Introduces an optional click-event pipeline: a ClickEvent model with Redis Stream wire codecs, a ClickEventSink abstraction (InlineSink/RedisStreamSink), redirect-route wiring to emit events with pre-emit bot blocking, worker-side consumers (StatsClickConsumer, HotUrlDetector) with dead-letter guarding and telemetry, queue Redis connection/version gating, configuration settings, compose services, and extensive supporting tests.

    Changes

    Click Event Pipeline

    Layer / File(s) Summary
    Configuration and queue setup
    config.py, infrastructure/queue_redis.py, infrastructure/cache/redis_client.py, app.py, pyproject.toml, .env.example, dependencies/wiring.py, tests/unit/infrastructure/*
    Adds ClickEventsSettings, queue Redis connection with version gating, labeled Redis client cleanup on failure, app.state.queue_redis lifecycle wiring, and sink selection in wire_services.
    Click event model and sink wiring
    services/click/events.py, services/click/sinks/*, services/click/protocol.py, services/click/service.py, services/click/handlers.py, dependencies/services.py, dependencies/__init__.py, services/click/bot_detection.py, services/click/consumers/protocol.py, services/click/consumers/__init__.py, tests/factories.py, tests/smoke/*, tests/unit/services/*
    Defines ClickEvent, stream encode/decode helpers, ClickEventSink/InlineSink/RedisStreamSink, should_block_bot, and switches ClickContext/track_click from start_time to redirect_ms.
    Redirect route emission
    routes/redirect_routes.py, tests/integration/*
    Redirect handler emits ClickEvent via ClickSink, blocks bots pre-redirect, and handles ValidationError/ForbiddenError distinctly during emission.
    Consumers, worker, DLQ, and telemetry
    services/click/consumers/stats.py, services/click/consumers/hotness.py, workers/click_worker.py, workers/dlq.py, workers/telemetry.py, tests/unit/workers/*, tests/unit/services/test_click_consumers.py
    Adds stats/hotness consumers, a FastStream worker hosting reader/claimer subscribers per group, ClaimDeadLetterGuard for stalled-message dead-lettering, and stream metrics/stale-consumer telemetry loops.
    Compose and infrastructure updates
    docker-compose.yml, docker-compose.prod.yml
    Adds redis-queue and click-worker services with persistent volumes, plus updates Redis healthcheck auth and Caddy memory limit in production compose.

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

    Sequence Diagram(s)

    sequenceDiagram
      participant Client
      participant RedirectRoute
      participant ClickSink
      participant RedisStream
      participant ClickWorker
      Client->>RedirectRoute: GET short_code
      RedirectRoute->>RedirectRoute: should_block_bot check
      RedirectRoute->>ClickSink: emit(ClickEvent)
      alt sink=stream
        ClickSink->>RedisStream: XADD click event
      else sink=inline or XADD fails
        ClickSink->>ClickSink: fallback to inline track_click
      end
      RedirectRoute-->>Client: 302 redirect
      RedisStream->>ClickWorker: XREADGROUP / XAUTOCLAIM
      ClickWorker->>ClickWorker: consume via stats/hotness consumer
    
    Loading

    Possibly related PRs

    • spoo-me/spoo#180: Both PRs modify V2ClickHandler.handle in services/click/handlers.py, one for redirect_ms and the other for domain-based cache handling.
    • spoo-me/spoo#188: Both PRs modify redirect_url in routes/redirect_routes.py, one rewiring click tracking to ClickSink and the other changing tenant-scoped domain resolution.

    Suggested labels: ✨ Refactor

    🚥 Pre-merge checks | ✅ 5
    ✅ Passed checks (5 passed)
    Check name Status Explanation
    Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
    Title check ✅ Passed The title matches the main change: introducing click event fanout through sinks and worker processing.
    Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
    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 docstrings
    • Create stacked PR
    • Commit on current branch
    🧪 Generate unit tests (beta)
    • Create PR with unit tests
    • Commit unit tests in branch feat/click-event-fanout

    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.

    @Zingzy Zingzy added priority: high High priority tasks backend Changes related to Backand/API labels Jul 3, 2026
    @Zingzy Zingzy moved this to 🏗️ In Progress in spoo.me Development Roadmap Jul 3, 2026

    @sourcery-ai sourcery-ai Bot left a comment

    Copy link
    Copy Markdown
    Contributor

    Choose a reason for hiding this comment

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

    Hey - I've found 2 issues

    Prompt for AI Agents
    Please address the comments from this code review:
    
    ## Individual Comments
    
    ### Comment 1
    <location path="docker-compose.prod.yml" line_range="239-240" />
    <code_context>
    +      --requirepass ${REDIS_QUEUE_PASSWORD}
    +    volumes:
    +      - redis-queue-data:/data
    +    healthcheck:
    +      test: ["CMD", "redis-cli", "ping"]
    +      interval: 10s
    +      timeout: 3s
    </code_context>
    <issue_to_address>
    **issue (bug_risk):** Healthcheck will fail against password-protected Redis
    
    Because Redis is started with `--requirepass ${REDIS_QUEUE_PASSWORD}`, `redis-cli ping` without `-a` will always return NOAUTH and mark the container unhealthy. Please update the healthcheck command to include the password (e.g. `redis-cli -a ${REDIS_QUEUE_PASSWORD} ping`) or another auth-aware check.
    </issue_to_address>
    
    ### Comment 2
    <location path="workers/click_worker.py" line_range="125-128" />
    <code_context>
    +        counter_redis=None,
    +    )
    +
    +    if "stats" in groups:
    +        geoip = GeoIPService(settings.geoip_country_db, settings.geoip_city_db)
    +        url_cache = UrlCache(cache_redis, ttl_seconds=settings.redis.redis_ttl_seconds)
    +        runtime.consumers["stats"] = StatsClickConsumer(
    +            build_click_service(
    +                ClickRepository(db["clicks"]),
    </code_context>
    <issue_to_address>
    **suggestion (bug_risk):** GeoIPService lifecycle in the worker is not explicitly closed
    
    `GeoIPService` is created here but not stored on `_WorkerRuntime` or closed on shutdown. If it holds file descriptors, threads, or MaxMind readers, this can leak resources over the worker’s lifetime or across restarts. Please retain it on `_WorkerRuntime` and close it in `aclose()`, consistent with the main app’s GeoIP teardown.
    
    Suggested implementation:
    
    ```python
        if "stats" in groups:
            geoip = GeoIPService(settings.geoip_country_db, settings.geoip_city_db)
    
            # Retain GeoIPService on the worker runtime so it can be closed on shutdown.
            # This mirrors the main app's GeoIP lifecycle management.
            runtime.geoip_service = geoip
    
            url_cache = UrlCache(cache_redis, ttl_seconds=settings.redis.redis_ttl_seconds)
            runtime.consumers["stats"] = StatsClickConsumer(
                build_click_service(
                    ClickRepository(db["clicks"]),
                    UrlRepository(db["urlsV2"]),
                    LegacyUrlRepository(db["urls"]),
                    EmojiUrlRepository(db["emojis"]),
                    geoip,
                    url_cache,
                )
            )
    
    ```
    
    To fully implement the suggestion, you should also:
    
    1. In the `_WorkerRuntime` class (likely in `workers/click_worker.py` or a nearby module):
       - Initialize the new attribute in `__init__` (or equivalent constructor) so it always exists:
         ```python
         self.geoip_service = None
         ```
       - Ensure any existing `geoip_service` passed in or set externally is assigned there if appropriate.
    
    2. In `_WorkerRuntime.aclose()` (or whatever shutdown/cleanup coroutine is used):
       - Add explicit teardown of the `GeoIPService`, following the same pattern as the main app:
         ```python
         async def aclose(self) -> None:
             # existing shutdown logic...
    
             if getattr(self, "geoip_service", None) is not None:
                 # Use the correct close method based on GeoIPService's API.
                 # If it is async:
                 await self.geoip_service.aclose()
                 # or, if it is sync:
                 # self.geoip_service.close()
         ```
       - If the main app uses a specific method name (e.g., `close()`, `aclose()`, `__aexit__`), mirror that here to keep behavior consistent.
    
    3. If `_WorkerRuntime` is instantiated elsewhere with a `geoip_service` argument or similar, align the constructor signature and attribute name with the usage above (`geoip_service`) to avoid confusion and keep naming consistent across the codebase.
    </issue_to_address>

    Sourcery is free for open source - if you like our reviews please consider sharing them ✨
    Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

    Comment thread docker-compose.prod.yml Outdated
    Comment thread workers/click_worker.py

    Copilot AI 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.

    Pull request overview

    This PR introduces a click-event fanout pipeline: redirect routes now emit immutable ClickEvent DTOs to a configurable sink (inline by default, Redis Stream when enabled), and a dedicated FastStream-based worker consumes the stream for analytics and optional hot-URL detection with DLQ handling.

    Changes:

    • Added ClickEvent DTO + ClickEventSink abstraction with inline and Redis Stream sink implementations (with inline fallback on stream failures).
    • Added a FastStream Redis click worker with per-group reader/claimer subscribers and a claim-path DLQ guard for poison messages.
    • Added click-event configuration (ClickEventsSettings), Redis client improvements (labeled logging + richer options), docker-compose wiring for optional queue Redis + worker, and expanded unit/integration/smoke test coverage.

    Reviewed changes

    Copilot reviewed 36 out of 39 changed files in this pull request and generated 3 comments.

    Show a summary per file
    File Description
    workers/dlq.py Adds claim-path DLQ guard for poison messages in Redis Stream consumer groups.
    workers/click_worker.py Introduces FastStream-based click worker app factory with reader/claimer subscribers and health route.
    workers/init.py Package marker for worker modules.
    uv.lock Locks new dependency set including FastStream (Redis).
    tests/unit/workers/test_dlq_guard.py Unit tests for DLQ guard behavior and failure modes.
    tests/unit/workers/test_click_worker_app.py Unit tests for worker app factory wiring and subscriber registration.
    tests/unit/test_click_events_settings.py Tests for ClickEventsSettings defaults and env parsing/validation.
    tests/unit/services/test_click_sinks.py Tests for inline vs stream sink behavior and fallback semantics.
    tests/unit/services/test_click_service.py Updates tests for redirect_ms-based click context plumbing.
    tests/unit/services/test_click_events.py Tests for ClickEvent immutability and stream wire encoding/decoding.
    tests/unit/services/test_click_consumers.py Tests for stats consumer replay semantics and hotness detector behavior.
    tests/smoke/test_click_sink_wiring.py Smoke test validating sink wiring and inline fallback in app composition.
    tests/integration/test_redirect.py Updates redirect integration tests to override get_click_sink instead of click service.
    tests/integration/test_redirect_routes.py Expands redirect-route tests to assert emitted ClickEvent snapshots and bot-block behavior.
    tests/integration/test_click_tracking.py Updates click-tracking integration tests to validate event emission and error handling.
    tests/conftest.py Adds app.state.click_sink default mock in test lifespan.
    services/click/sinks/stream.py Implements Redis Stream sink with warning log + inline fallback on any XADD failure.
    services/click/sinks/protocol.py Adds sink protocol boundary for redirect → click processing.
    services/click/sinks/inline.py Implements inline sink that replays event into ClickService.track_click.
    services/click/sinks/init.py Exposes sink types from package.
    services/click/service.py Switches click context parameter from start_time to redirect_ms.
    services/click/protocol.py Updates ClickContext to carry redirect_ms rather than start_time.
    services/click/handlers.py Uses context.redirect_ms rather than recomputing from perf counter.
    services/click/events.py Adds ClickEvent DTO and Redis Stream wire helpers.
    services/click/consumers/stats.py Adds stats consumer that replays events via ClickService with drop vs retry semantics.
    services/click/consumers/hotness.py Adds hot-URL detection consumer using Redis window counters and pluggable actions.
    services/click/consumers/init.py Exports click consumer classes/types.
    routes/redirect_routes.py Refactors redirect route to emit ClickEvent through sink + pre-emit bot blocking for v1/emoji.
    pyproject.toml Adds faststream[redis] dependency.
    infrastructure/cache/redis_client.py Enhances Redis client creation (labels, keepalive, health checks, richer logs).
    docker-compose.yml Adds opt-in queue Redis + click-worker service behind click-events profile for local runs.
    docker-compose.prod.yml Adds production queue Redis + click-worker service configuration and health checks.
    dependencies/wiring.py Centralizes ClickService construction and wires click sink based on settings + queue Redis availability.
    dependencies/services.py Adds get_click_sink dependency and ClickSink annotated type.
    dependencies/init.py Re-exports new click sink dependency symbols.
    config.py Adds ClickEventsSettings and integrates it into AppSettings composition.
    app.py Connects optional queue Redis for stream mode and closes it on shutdown.
    .env.example Documents environment variables for enabling the click event pipeline.

    💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

    Comment thread workers/dlq.py
    Comment thread workers/dlq.py
    Comment thread config.py

    @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

    🧹 Nitpick comments (7)
    docker-compose.prod.yml (1)

    239-244: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

    Healthcheck doesn't authenticate against the password-protected queue Redis.

    redis-queue is started with --requirepass ${REDIS_QUEUE_PASSWORD}, but the healthcheck runs bare redis-cli ping. Redis returns NOAUTH Authentication required for this, and redis-cli exits with code 0 on that error — the container is reported "healthy" even if credentials are wrong/misconfigured, defeating the purpose of the check. This mirrors the existing spoo_redis service's healthcheck but is reproduced here for the new redis-queue service.

    🔒 Proposed fix — authenticate the healthcheck
         healthcheck:
    -      test: ["CMD", "redis-cli", "ping"]
    +      test: ["CMD", "redis-cli", "-a", "${REDIS_QUEUE_PASSWORD}", "--no-auth-warning", "ping"]
           interval: 10s
           timeout: 3s
           retries: 5
           start_period: 5s
    🤖 Prompt for 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.
    
    In `@docker-compose.prod.yml` around lines 239 - 244, The healthcheck for
    redis-queue is currently using a plain redis-cli ping and does not authenticate,
    so it can report healthy even when the password is wrong. Update the redis-queue
    service’s healthcheck to pass the queue password when calling redis-cli,
    matching the authenticated pattern used by spoo_redis, and keep the fix
    localized to the healthcheck test block for that service.
    
    workers/dlq.py (1)

    30-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

    Unused helper _as_str.

    Not referenced anywhere in this file — _times_delivered only uses times_delivered (already handled via int(times), Line 113), never message_id/consumer byte-decoding.

    🤖 Prompt for 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.
    
    In `@workers/dlq.py` around lines 30 - 33, The helper _as_str in dlq.py is unused
    and should be removed. Delete the _as_str function and verify there are no
    remaining references in the DLQ module, since _times_delivered already handles
    its parsing directly with int(times) and does not need byte-decoding for
    message_id or consumer.
    
    workers/click_worker.py (2)

    103-158: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

    Mongo/cache-Redis connections are opened even when unused.

    mongo_client and cache_redis are always created, but only used when "stats" is in groups. A hotness-only worker (a scenario the module's own docstring anticipates — "a future deployment can split groups across containers", Lines 25-28) still pays for an unused Mongo pool and Redis connection.

    ♻️ Proposed fix: build Mongo/cache-Redis only when stats is enabled
    -    ce = settings.click_events
    -    mongo_client: AsyncMongoClient = AsyncMongoClient(
    -        settings.db.mongodb_uri,
    -        maxPoolSize=_WORKER_MONGO_MAX_POOL,
    -        minPoolSize=1,
    -    )
    -    db = mongo_client[settings.db.db_name]
    -
    -    # Cache Redis is optional in the worker exactly as in the web app —
    -    # without it the URL cache degrades to no-ops (max-clicks expiry just
    -    # skips cache invalidation; resolve-side caching is the app's concern).
    -    cache_redis = None
    -    if settings.redis.redis_uri:
    -        cache_redis = await create_redis_client(settings.redis.redis_uri, label="cache")
    -
    -    runtime = _WorkerRuntime(
    -        mongo_client=mongo_client,
    -        cache_redis=cache_redis,
    -        counter_redis=None,
    -    )
    -
    -    if "stats" in groups:
    +    ce = settings.click_events
    +    mongo_client: AsyncMongoClient | None = None
    +    cache_redis = None
    +
    +    if "stats" in groups:
    +        mongo_client = AsyncMongoClient(
    +            settings.db.mongodb_uri,
    +            maxPoolSize=_WORKER_MONGO_MAX_POOL,
    +            minPoolSize=1,
    +        )
    +        db = mongo_client[settings.db.db_name]
    +        if settings.redis.redis_uri:
    +            cache_redis = await create_redis_client(settings.redis.redis_uri, label="cache")
    +
    +    runtime = _WorkerRuntime(
    +        mongo_client=mongo_client,
    +        cache_redis=cache_redis,
    +        counter_redis=None,
    +    )
    +
    +    if "stats" in groups:

    (_WorkerRuntime.aclose would also need a None guard for mongo_client.)

    🤖 Prompt for 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.
    
    In `@workers/click_worker.py` around lines 103 - 158, The `_build_runtime` setup
    eagerly opens `AsyncMongoClient` and the optional cache Redis even when only
    `"hotness"` is requested, so move those connection initializations behind the
    `"stats" in groups` branch and only pass them into `StatsClickConsumer` when
    needed. Update `_WorkerRuntime` so `mongo_client`/`cache_redis` can be absent in
    hotness-only runs, and add the corresponding `None` guard in
    `_WorkerRuntime.aclose` before closing the Mongo client.
    

    239-247: 🩺 Stability & Availability | 🔵 Trivial

    Stale consumer names accumulate in the Redis consumer group across restarts.

    consumer_suffix is derived from hostname-pid (Line 245), so every process restart registers a brand-new consumer name; old ones are never removed via XGROUP DELCONSUMER. Functionally harmless (XAUTOCLAIM reclaims by idle time regardless of owner), but XINFO CONSUMERS will grow unbounded over the worker's lifetime — worth a periodic cleanup job or accepting the metadata growth.

    🤖 Prompt for 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.
    
    In `@workers/click_worker.py` around lines 239 - 247, The Redis consumer group
    setup in click_worker.consumer_for and the consumer_suffix registration
    currently creates a new consumer name on every restart, leaving stale entries
    behind. Fix this by adding a cleanup path that removes old consumers with XGROUP
    DELCONSUMER (or by reusing a stable consumer identity) when registering groups
    in _register_group / the group registration loop. Make sure the cleanup runs
    periodically or during startup before _register_group is called so XINFO
    CONSUMERS does not grow unbounded.
    
    app.py (1)

    118-129: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

    Consider reusing create_redis_client for the primary cache client too.

    The queue Redis client correctly adopts the new create_redis_client(..., label=...) helper, but the primary redis_client a few lines above (94-116) still hand-rolls the identical from_url + ping + try/except logic. Now that the helper exists (and is used here), consolidating both call sites would remove duplication and give the cache client the same structured label/error_type logging.

    ♻️ Proposed refactor
    -        redis_client = None
    -        if settings.redis.redis_uri:
    -            redis_client = aioredis.from_url(
    -                settings.redis.redis_uri,
    -                encoding="utf-8",
    -                decode_responses=True,
    -                socket_keepalive=True,
    -                health_check_interval=30,
    -            )
    -            try:
    -                await redis_client.ping()
    -                log.info(
    -                    "redis_connected", ttl_seconds=settings.redis.redis_ttl_seconds
    -                )
    -            except Exception as e:
    -                log.error("redis_connection_failed", error=str(e))
    -                redis_client = None
    -        else:
    +        redis_client = None
    +        if settings.redis.redis_uri:
    +            redis_client = await create_redis_client(
    +                settings.redis.redis_uri, label="cache"
    +            )
    +        else:
                 log.warning(
                     "redis_not_configured", detail="set REDIS_URI to enable caching"
                 )
    🤖 Prompt for 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.
    
    In `@app.py` around lines 118 - 129, The queue Redis setup already uses
    create_redis_client with a label, but the primary cache client in app.py still
    duplicates the same from_url/ping/try-except flow. Refactor the cache-client
    initialization to call create_redis_client as well, so both the main
    redis_client and the click-events queue_redis share the same helper, structured
    label, and error_type logging.
    
    services/click/sinks/stream.py (1)

    36-51: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

    Broad except Exception masks non-Redis failures.

    Catching all exceptions around xadd means bugs unrelated to Redis connectivity (e.g., a future serialization issue in to_stream_fields) are silently routed through the fallback rather than surfaced distinctly. Consider narrowing to redis.exceptions.RedisError (and any connection-specific exceptions) so unexpected errors aren't masked by the "degrade gracefully" path.

    🤖 Prompt for 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.
    
    In `@services/click/sinks/stream.py` around lines 36 - 51, The emit method in
    ClickSink is catching too broadly and masking non-Redis bugs. Narrow the
    exception handling around the self._redis.xadd call in emit to Redis-specific
    failures (for example RedisError and any connection-related subclasses) so
    issues in to_stream_fields or other unexpected errors are not silently sent
    through _fallback.emit. Keep the fallback path for genuine Redis transport/write
    failures, and let non-Redis exceptions surface normally.
    
    services/click/events.py (1)

    41-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

    Nested UrlCacheData is not itself frozen.

    ClickEvent uses ConfigDict(frozen=True), but the embedded url: UrlCacheData field isn't guaranteed immutable at its own model level, so a consumer holding a reference could still mutate event.url in place, breaking the "immutable fact" guarantee documented at the top of the file.

    🤖 Prompt for 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.
    
    In `@services/click/events.py` around lines 41 - 58, ClickEvent is marked frozen,
    but its nested url field can still be mutated if UrlCacheData itself is not
    frozen. Update the UrlCacheData model to be immutable as well, and confirm the
    ClickEvent model in events.py continues to use it as the embedded type so the
    full event remains an immutable fact.
    
    🤖 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 `@infrastructure/cache/redis_client.py`:
    - Around line 35-49: The Redis connection handling in `from_url()` leaves the
    created client/pool open when `ping()` fails, because the `RedisError` and
    generic `Exception` branches return `None` without cleanup. Update those
    exception paths in `redis_client.py` to close the constructed client by calling
    `await client.aclose()` before returning, so any partially initialized Redis
    client is always released after a failed `ping()`.
    
    In `@services/click/events.py`:
    - Around line 70-86: The malformed-event logging in from_stream_fields and the
    matching click_event_from_payload path should not include slices of the raw
    payload, because that can expose PII from ClickEvent data. Update the warning
    logs in these branches to emit only structural diagnostics such as the
    validation error, payload type, or field names, and keep any raw content fully
    redacted or omitted. Use the existing from_stream_fields helper and the
    analogous click_event_from_payload branch as the places to change so both JSON
    and dict parsing paths behave consistently.
    
    In `@services/click/sinks/stream.py`:
    - Around line 36-43: The Redis client used by the click stream path is created
    without a socket timeout, so `ClickStreamSink.emit()` can block indefinitely on
    `xadd`. Update the Redis client configuration in `redis_client.py` to set a
    bounded `socket_timeout` (and keep it appropriate for the redirect path), so
    `emit()` can fail fast and continue with the inline fallback instead of hanging.
    
    ---
    
    Nitpick comments:
    In `@app.py`:
    - Around line 118-129: The queue Redis setup already uses create_redis_client
    with a label, but the primary cache client in app.py still duplicates the same
    from_url/ping/try-except flow. Refactor the cache-client initialization to call
    create_redis_client as well, so both the main redis_client and the click-events
    queue_redis share the same helper, structured label, and error_type logging.
    
    In `@docker-compose.prod.yml`:
    - Around line 239-244: The healthcheck for redis-queue is currently using a
    plain redis-cli ping and does not authenticate, so it can report healthy even
    when the password is wrong. Update the redis-queue service’s healthcheck to pass
    the queue password when calling redis-cli, matching the authenticated pattern
    used by spoo_redis, and keep the fix localized to the healthcheck test block for
    that service.
    
    In `@services/click/events.py`:
    - Around line 41-58: ClickEvent is marked frozen, but its nested url field can
    still be mutated if UrlCacheData itself is not frozen. Update the UrlCacheData
    model to be immutable as well, and confirm the ClickEvent model in events.py
    continues to use it as the embedded type so the full event remains an immutable
    fact.
    
    In `@services/click/sinks/stream.py`:
    - Around line 36-51: The emit method in ClickSink is catching too broadly and
    masking non-Redis bugs. Narrow the exception handling around the
    self._redis.xadd call in emit to Redis-specific failures (for example RedisError
    and any connection-related subclasses) so issues in to_stream_fields or other
    unexpected errors are not silently sent through _fallback.emit. Keep the
    fallback path for genuine Redis transport/write failures, and let non-Redis
    exceptions surface normally.
    
    In `@workers/click_worker.py`:
    - Around line 103-158: The `_build_runtime` setup eagerly opens
    `AsyncMongoClient` and the optional cache Redis even when only `"hotness"` is
    requested, so move those connection initializations behind the `"stats" in
    groups` branch and only pass them into `StatsClickConsumer` when needed. Update
    `_WorkerRuntime` so `mongo_client`/`cache_redis` can be absent in hotness-only
    runs, and add the corresponding `None` guard in `_WorkerRuntime.aclose` before
    closing the Mongo client.
    - Around line 239-247: The Redis consumer group setup in
    click_worker.consumer_for and the consumer_suffix registration currently creates
    a new consumer name on every restart, leaving stale entries behind. Fix this by
    adding a cleanup path that removes old consumers with XGROUP DELCONSUMER (or by
    reusing a stable consumer identity) when registering groups in _register_group /
    the group registration loop. Make sure the cleanup runs periodically or during
    startup before _register_group is called so XINFO CONSUMERS does not grow
    unbounded.
    
    In `@workers/dlq.py`:
    - Around line 30-33: The helper _as_str in dlq.py is unused and should be
    removed. Delete the _as_str function and verify there are no remaining
    references in the DLQ module, since _times_delivered already handles its parsing
    directly with int(times) and does not need byte-decoding for message_id or
    consumer.
    
    🪄 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: defaults

    Review profile: CHILL

    Plan: Pro

    Run ID: 27893ebc-e566-4944-b722-f57fc6cb9ddf

    📥 Commits

    Reviewing files that changed from the base of the PR and between 5fb79f9 and af1b1e4.

    ⛔ Files ignored due to path filters (1)
    • uv.lock is excluded by !**/*.lock
    📒 Files selected for processing (38)
    • .env.example
    • app.py
    • config.py
    • dependencies/__init__.py
    • dependencies/services.py
    • dependencies/wiring.py
    • docker-compose.prod.yml
    • docker-compose.yml
    • infrastructure/cache/redis_client.py
    • pyproject.toml
    • routes/redirect_routes.py
    • services/click/consumers/__init__.py
    • services/click/consumers/hotness.py
    • services/click/consumers/stats.py
    • services/click/events.py
    • services/click/handlers.py
    • services/click/protocol.py
    • services/click/service.py
    • services/click/sinks/__init__.py
    • services/click/sinks/inline.py
    • services/click/sinks/protocol.py
    • services/click/sinks/stream.py
    • tests/conftest.py
    • tests/integration/test_click_tracking.py
    • tests/integration/test_redirect.py
    • tests/integration/test_redirect_routes.py
    • tests/smoke/test_click_sink_wiring.py
    • tests/unit/services/test_click_consumers.py
    • tests/unit/services/test_click_events.py
    • tests/unit/services/test_click_service.py
    • tests/unit/services/test_click_sinks.py
    • tests/unit/test_click_events_settings.py
    • tests/unit/workers/__init__.py
    • tests/unit/workers/test_click_worker_app.py
    • tests/unit/workers/test_dlq_guard.py
    • workers/__init__.py
    • workers/click_worker.py
    • workers/dlq.py

    Comment thread infrastructure/cache/redis_client.py
    Comment thread services/click/events.py
    Comment thread services/click/sinks/stream.py
    Zingzy added 5 commits July 4, 2026 01:32
    … policy
    
    Replaces the client-side history sweeper: XADD ref_policy=ACKED sweeps
    fully-acknowledged entries past maxlen on every publish, server-side.
    Queue redis now requires >= 8.2 (gated at startup with inline fallback).
    Password-hash sanitization moves into the event model (structural, not
    caller-remembered); the consumer protocol lives with its implementers;
    the queue redis gate is an extracted, tested module; the pre-emit bot
    decision is a named predicate; shared test factories replace per-file
    copies.

    @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: 1

    🧹 Nitpick comments (1)
    infrastructure/queue_redis.py (1)

    23-23: 🧹 Nitpick | 🔵 Trivial

    Known Redis caveat with approximate ACKED trimming worth confirming.

    Redis has an open bug where XADD/XTRIM MAXLEN ~ ACKED (approximate trimming) can fail to evict entries even when fully acknowledged, first observed on 8.2.2 and reproduced on 8.4.0. Since this whole gate exists specifically to enable ACKED-based trimming, it's worth confirming the deployed queue-Redis version/patch level isn't affected, or that exact trimming is used as a fallback if unacked backlog needs bounding.

    🤖 Prompt for 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.
    
    In `@infrastructure/queue_redis.py` at line 23, The Redis version gate in
    MIN_QUEUE_REDIS_VERSION should account for the known approximate ACKED trimming
    bug on affected 8.2/8.4 patch levels. Update the version check logic in
    infrastructure/queue_redis.py to either require a safe fixed Redis patch level
    for the ACKED trimming path or add a fallback to exact trimming when the
    deployed version may be impacted. Refer to the queue Redis gating around
    MIN_QUEUE_REDIS_VERSION and the ACKED-based trimming feature so the
    compatibility check remains aligned with the actual Redis behavior.
    
    🤖 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 `@workers/click_worker.py`:
    - Around line 84-93: The telemetry shutdown in aclose cancels telemetry_tasks
    but does not wait for them to finish, so they can still be running when
    telemetry_redis is closed. Update aclose to cancel the tasks and then await
    their completion (for example by gathering them and handling cancellation)
    before closing telemetry_redis. Keep the existing close order for mongo_client,
    cache_redis, counter_redis, and telemetry_redis, but ensure the telemetry_tasks
    drain first.
    
    ---
    
    Nitpick comments:
    In `@infrastructure/queue_redis.py`:
    - Line 23: The Redis version gate in MIN_QUEUE_REDIS_VERSION should account for
    the known approximate ACKED trimming bug on affected 8.2/8.4 patch levels.
    Update the version check logic in infrastructure/queue_redis.py to either
    require a safe fixed Redis patch level for the ACKED trimming path or add a
    fallback to exact trimming when the deployed version may be impacted. Refer to
    the queue Redis gating around MIN_QUEUE_REDIS_VERSION and the ACKED-based
    trimming feature so the compatibility check remains aligned with the actual
    Redis behavior.
    
    🪄 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: defaults

    Review profile: CHILL

    Plan: Pro

    Run ID: 792346d0-d4b4-427f-97a2-28625d3aabac

    📥 Commits

    Reviewing files that changed from the base of the PR and between af1b1e4 and 63427e2.

    📒 Files selected for processing (27)
    • app.py
    • config.py
    • docker-compose.prod.yml
    • docker-compose.yml
    • infrastructure/queue_redis.py
    • k6/scenarios/_loadcmp.js
    • k6/scenarios/_realistic.js
    • routes/redirect_routes.py
    • services/click/bot_detection.py
    • services/click/consumers/__init__.py
    • services/click/consumers/protocol.py
    • services/click/events.py
    • services/click/sinks/stream.py
    • tests/factories.py
    • tests/integration/test_click_tracking.py
    • tests/integration/test_redirect.py
    • tests/integration/test_redirect_routes.py
    • tests/unit/infrastructure/test_queue_redis.py
    • tests/unit/services/test_bot_detection.py
    • tests/unit/services/test_click_consumers.py
    • tests/unit/services/test_click_events.py
    • tests/unit/services/test_click_sinks.py
    • tests/unit/test_click_events_settings.py
    • tests/unit/workers/test_telemetry.py
    • workers/click_worker.py
    • workers/dlq.py
    • workers/telemetry.py
    🚧 Files skipped from review as they are similar to previous changes (15)
    • services/click/consumers/init.py
    • tests/unit/test_click_events_settings.py
    • tests/unit/services/test_click_events.py
    • services/click/sinks/stream.py
    • docker-compose.yml
    • tests/unit/services/test_click_sinks.py
    • app.py
    • docker-compose.prod.yml
    • services/click/events.py
    • tests/integration/test_click_tracking.py
    • config.py
    • tests/integration/test_redirect.py
    • tests/unit/services/test_click_consumers.py
    • tests/integration/test_redirect_routes.py
    • workers/dlq.py

    Comment thread workers/click_worker.py
    @Zingzy
    Zingzy force-pushed the feat/click-event-fanout branch from 6d5c87e to 86a46d8 Compare July 3, 2026 22:11
    @Zingzy
    Zingzy merged commit 7f0167b into main Jul 3, 2026
    11 of 12 checks passed
    @github-project-automation github-project-automation Bot moved this from 🏗️ In Progress to ✔️ Done in spoo.me Development Roadmap Jul 3, 2026

    @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.

    🧹 Nitpick comments (1)
    docker-compose.prod.yml (1)

    276-281: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

    Prefer the redis-queue service name over the hardcoded static IP.

    CLICK_EVENTS_QUEUE_REDIS_URI uses 172.30.0.45 directly, while the rest of the file (e.g., redis-exporter's REDIS_ADDR=redis://redis:6379) resolves peers by service name over Docker's embedded DNS. Works today since the IP is statically pinned via ipv4_address, but ties this env var to network config that lives elsewhere in the file, risking silent breakage if the IP is ever changed without updating this string.

    ♻️ Proposed change
    -      CLICK_EVENTS_QUEUE_REDIS_URI: redis://:${REDIS_QUEUE_PASSWORD}`@172.30.0.45`:6379/0
    +      CLICK_EVENTS_QUEUE_REDIS_URI: redis://:${REDIS_QUEUE_PASSWORD}`@redis-queue`:6379/0
    🤖 Prompt for 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.
    
    In `@docker-compose.prod.yml` around lines 276 - 281, The
    CLICK_EVENTS_QUEUE_REDIS_URI value is hardcoded to a static IP, which should
    instead use the redis-queue service name like the other Docker service
    references in docker-compose.prod.yml. Update the environment entry in the
    relevant compose service to point at redis-queue via Docker DNS, using the
    existing CLICK_EVENTS_QUEUE_REDIS_URI setting and preserving the Redis password
    interpolation.
    
    🤖 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.
    
    Nitpick comments:
    In `@docker-compose.prod.yml`:
    - Around line 276-281: The CLICK_EVENTS_QUEUE_REDIS_URI value is hardcoded to a
    static IP, which should instead use the redis-queue service name like the other
    Docker service references in docker-compose.prod.yml. Update the environment
    entry in the relevant compose service to point at redis-queue via Docker DNS,
    using the existing CLICK_EVENTS_QUEUE_REDIS_URI setting and preserving the Redis
    password interpolation.
    

    ℹ️ Review info
    ⚙️ Run configuration

    Configuration used: defaults

    Review profile: CHILL

    Plan: Pro

    Run ID: 5315977c-10f6-4755-83d2-f798ff529397

    📥 Commits

    Reviewing files that changed from the base of the PR and between 63427e2 and 8c155b0.

    📒 Files selected for processing (7)
    • docker-compose.prod.yml
    • infrastructure/cache/redis_client.py
    • services/click/events.py
    • tests/unit/infrastructure/test_redis_client.py
    • tests/unit/workers/test_dlq_guard.py
    • workers/click_worker.py
    • workers/dlq.py
    🚧 Files skipped from review as they are similar to previous changes (4)
    • infrastructure/cache/redis_client.py
    • services/click/events.py
    • tests/unit/workers/test_dlq_guard.py
    • workers/click_worker.py

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    backend Changes related to Backand/API priority: high High priority tasks

    Projects

    Status: ✔️ Done

    Development

    Successfully merging this pull request may close these issues.

    2 participants