Skip to content

fix: harden analytics delivery - #581

Closed
izadoesdev wants to merge 25 commits into
stagingfrom
codex/reliability-hardening
Closed

fix: harden analytics delivery#581
izadoesdev wants to merge 25 commits into
stagingfrom
codex/reliability-hardening

Conversation

@izadoesdev

@izadoesdev izadoesdev commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

  • make Railway-safe Vector delivery rely on Redpanda and acknowledged ClickHouse writes
  • checkpoint uptime checks in BullMQ before durable event relay, with idempotent replay handling
  • deduplicate replayed uptime analytics at query time
  • make Basket core event admission await durable delivery, owner-safe dedupe release, shutdown draining, and pixel retries

Constraints

  • no migrations
  • no new PostgreSQL delivery storage or outbox tables

Verification

  • bun run lint
  • bun run check-types
  • full pre-push test matrix
  • focused Basket, tracker, uptime, Redis, AI, and RPC suites

Summary by cubic

Strengthened analytics and uptime delivery: Redpanda remains the durable source with ClickHouse‑acknowledged sinks, Basket drains on shutdown and prevents concurrent delivery, and uptime checks relay through BullMQ with validated payloads and long‑lived retries.

  • Refactors

    • Basket: simplified acknowledged delivery and shutdown drain; removed the effect runtime; clarified direct ClickHouse fallback behavior; stable IDs via stableAnalyticsEventId with batch sourceEventId.
    • Uptime: simplified durable delivery flow and producer; added separate delivery worker; fixed‑delay retry options; strict zod validation for job envelopes and payloads.
    • Ingest: cleaned up Vector config defaults and removed dead sinks; acknowledgements stay enabled so offsets advance only after ClickHouse confirms.
    • Analytics: consolidated uptime replay dedupe queries using shared TABLE_COLUMNS across @databuddy/ai and @databuddy/rpc.
    • Tracker: removed redundant Beacon delivery path so outcomes follow HTTP results; pixel plugin retains capped retries and cancels on navigation.
  • Migration

    • No database migrations.
    • Deploy the updated infra/ingest/vector.yaml with acknowledgements enabled.

Written for commit 844f2ec. Summary will update on new commits.

Review in cubic

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboard Ready Ready Preview Aug 1, 2026 4:26pm
databuddy-status Ready Ready Preview Aug 1, 2026 4:26pm
documentation Ready Ready Preview Aug 1, 2026 4:26pm

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6a53758e-b1db-4207-9aaf-f6fde10d9452

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@unkey-deploy

unkey-deploy Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Unkey Deploy

Name Status Preview Inspect Updated (UTC)
links (preview) Ready Visit Preview Inspect Aug 1, 2026 4:25pm

@cursor

cursor Bot commented Jul 31, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@dosubot

dosubot Bot commented Jul 31, 2026

Copy link
Copy Markdown

📄 Knowledge review

Dosu skipped reviewing this PR because your organization has used its 200 included credits for the month. Your usage will reset on 2026-08-01. To have Dosu review this PR before then, ask your organization admin to upgrade to a pro account.


Leave Feedback Ask Dosu about Databuddy Add Dosu to your team

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR hardens analytics and uptime delivery across ingestion, relay, querying, and browser retry paths.

  • Adds acknowledged Redpanda/ClickHouse delivery, stable analytics IDs, Redis reservation-based deduplication, and shutdown draining to Basket.
  • Checkpoints uptime results into BullMQ and relays them through a dedicated durable delivery worker.
  • Deduplicates replayed uptime records in query builders and status-page queries.
  • Adds retry-aware pixel transport behavior and acknowledged Vector sinks.

Confidence Score: 4/5

The concurrent pending-reservation path must be fixed before merging because it allows the same core analytics event to be durably delivered more than once.

A second request observing an existing pending Redis token is admitted without reservation ownership, so concurrent retries can both publish the same stable event into tables that retain duplicate rows.

Files Needing Attention: apps/basket/src/lib/security.ts, apps/basket/src/lib/event-service.ts

Important Files Changed

Filename Overview
apps/basket/src/lib/security.ts Introduces owner-safe reservation release but admits concurrent requests that observe another request's pending reservation.
apps/basket/src/lib/event-service.ts Adds stable IDs and awaits durable delivery before confirming core-event reservations.
apps/basket/src/lib/producer.ts Reworks Kafka fallback, buffering, acknowledgement, and shutdown draining around explicit producer state.
apps/uptime/src/worker.ts Adds checkpointed BullMQ handoff, replay handling, and a dedicated uptime delivery worker.
packages/redis/uptime-queue.ts Adds durable delivery queue configuration and stable delivery job IDs.
packages/tracker/src/plugins/pixel.ts Adds bounded retry behavior, Retry-After handling, and navigation cancellation for pixel requests.
infra/ingest/vector.yaml Enables acknowledged ClickHouse sinks with memory buffering so Kafka offsets follow confirmed writes.

Sequence Diagram

sequenceDiagram
    participant T as Tracker
    participant B as Basket
    participant R as Redis dedup
    participant K as Redpanda
    participant C as ClickHouse
    T->>B: analytics event
    B->>R: reserve stable event ID
    R-->>B: acquired / pending / delivered
    B->>K: acknowledged publish
    alt Kafka unavailable
        B->>C: acknowledged direct insert
    end
    B->>R: mark delivered
    B-->>T: success or retryable 503
Loading

Reviews (1): Last reviewed commit: "fix(basket): durably admit core analytic..." | Re-trigger Greptile

Comment thread apps/basket/src/lib/security.ts Outdated
Comment on lines +216 to +222
// than falsely reporting success for an event that never reached storage.
return {
duplicate: false,
key,
token: result === "acquired" ? token : undefined,
ttl,
};

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.

P1 Pending reservations admit duplicates

When two requests submit the same client, event type, and event ID concurrently, the second request treats the first request's pending token as duplicate: false and proceeds without owning the reservation. Both requests can durably publish the same stable ID into MergeTree-backed analytics tables, producing duplicate rows and inflated analytics.

Knowledge Base Used: Basket Ingestion Flow

@cubic-dev-ai cubic-dev-ai 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.

12 issues found across 39 files

Confidence score: 2/5

  • In packages/tracker/src/plugins/pixel.ts, in-flight pixel requests can still complete after opt-out/clear, and deliveries can hang indefinitely on unresolved image events, which risks both privacy leakage and a stuck tracker queue; track/cancel active Image requests, add timeout/abort handling, and sanitize maxRetries to a finite non-negative integer.
  • In apps/basket/src/lib/event-service.ts, releasing the retry guard after transport-level failures can replay events that were already accepted, inflating analytics totals in ClickHouse/Redpanda; keep the guard aligned to durable delivery semantics or add stronger idempotency on retries.
  • In apps/basket/src/lib/security.ts, legacy dedup values ("1") are treated as someone else’s pending reservation, so valid retries fail for the full TTL and can drop expected reprocessing; treat the legacy delivered marker as delivered during the migration window.
  • In apps/basket/src/index.ts, shutdown now leaves very little time after producer drain before hard termination, increasing the chance Redis/Postgres/log flushes are cut off; rebalance the shutdown timeout budget so serial drain and cleanup can complete reliably.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/ai/src/query/builders/uptime.ts">

<violation number="1" location="packages/ai/src/query/builders/uptime.ts:21">
P3: This change adds the same 22-column `UPTIME_REPLAY_IDENTITY` and the same `uptimeEventSource()` helper in two separate packages (ai and rpc) with no shared source of truth or cross-reference comment. If a column is ever added to the table or the replay identity list is adjusted in one file, the two sites will silently deduplicate differently (query-time anti-replay behavior diverges), and the divergence won't be caught. Since the two packages can't easily share a module, at minimum add a cross-reference comment on each definition pointing at the other location (and at the schema file) so future edits prompt checking the paired copy.</violation>

<violation number="2" location="packages/ai/src/query/builders/uptime.ts:58">
P2: Dashboard queries sort the entire selected uptime range before deduplication, adding avoidable work to every uptime aggregate and window query. Since the identity covers every stored column, selection order cannot affect the retained row; remove this inner sort and let consumers that need presentation ordering keep their outer `ORDER BY`.</violation>
</file>

<file name="apps/basket/src/index.ts">

<violation number="1" location="apps/basket/src/index.ts:89">
P2: A full producer drain now leaves less than two seconds before the hard shutdown timer terminates Redis/Postgres and log flushing. Consider budgeting the process timeout for this serial drain plus cleanup, so a successful near-deadline delivery does not routinely skip the remaining shutdown work.</violation>
</file>

<file name="packages/rpc/src/routers/status-page.ts">

<violation number="1" location="packages/rpc/src/routers/status-page.ts:72">
P2: Public status-page refreshes now sort every selected check before deduplication, although each `LIMIT BY` key contains every physical `uptime_monitor` column. Exact duplicates are indistinguishable downstream, so remove this sort to avoid unnecessary sort and memory work for the 90-day query.</violation>
</file>

<file name="apps/basket/src/lib/security.ts">

<violation number="1" location="apps/basket/src/lib/security.ts:165">
P2: Retries matching dedup keys created before this rollout return retryable failures for the rest of their TTL, because prior delivered value `"1"` is classified as another request's pending reservation. Treat that legacy value as delivered during the transition.</violation>
</file>

<file name="packages/tracker/src/plugins/pixel.ts">

<violation number="1" location="packages/tracker/src/plugins/pixel.ts:67">
P2: A negative or non-finite runtime `maxRetries` can drop every pixel without even one delivery attempt. Normalize it to a finite non-negative integer before using it as the loop bound.</violation>

<violation number="2" location="packages/tracker/src/plugins/pixel.ts:116">
P2: Every delivery now blocks on the Image onload/onerror promise with no timeout or abort path, and a pixel whose load never settles leaves BaseTracker's queue permanently stuck. Because sendBeacon now returns false unconditionally, the unload/destroy and normal flush paths all go through this api.fetch image-load promise. If a request is aborted by navigation or the browser never fires onload/onerror for it, `await load()` never resolves, so `api.fetch` never returns. In `_flushQueue`, the `finally` block only clears `meta.flushing` when the fetch resolves — so this queue stays `flushing: true` forever and every subsequent event is queued but never sent. The previous sendBeacon path returned `true` immediately (clearing the queue) and fired the Image in the background, so this hang could not previously brick a queue. Consider wrapping `load()` in a timeout (e.g. racing against the existing MAX_PIXEL_RETRY_DELAY_MS) and treating a timeout as a failed attempt so the promise always settles.</violation>

<violation number="3" location="packages/tracker/src/plugins/pixel.ts:124">
P1: Opting out or clearing during an in-flight pixel load still lets that pixel request finish and transmit its event. Track active `Image` instances and cancel them when `cancelPendingRequests` runs; generation checks only prevent later retries.</violation>
</file>

<file name="apps/basket/src/lib/event-service.ts">

<violation number="1" location="apps/basket/src/lib/event-service.ts:225">
P1: Ambiguous delivery retries can inflate core analytics: this releases the guard after a rejection even if ClickHouse/Redpanda accepted the event before the response failed. Stable IDs alone do not deduplicate these `ReplicatedMergeTree` rows, so preserve an ambiguous reservation or add ID-level deduplication before permitting retry.</violation>
</file>

<file name="apps/uptime/src/worker.ts">

<violation number="1" location="apps/uptime/src/worker.ts:448">
P2: Corrupt or legacy persisted delivery jobs containing only these three fields pass validation and are sent to Redpanda as `UptimeData`. Validate every required `UptimeData` field (and `json_data` when present) before enqueue/replay so invalid data fails the job instead.

(Based on your team's feedback about runtime type validation.) [FEEDBACK_USED].</violation>
</file>

<file name="packages/ai/src/ai/agents/cache.test.ts">

<violation number="1" location="packages/ai/src/ai/agents/cache.test.ts:58">
P2: Spreading `...redisModule` into the mock re-exposes the real Redis-backed implementations that the old per-export stubs deliberately isolated. The internal redis functions (`cacheable`, the `invalidate*` helpers, and the `redis` proxy) call the module's own `getRedisCache`, which constructs a real `ioredis` client — they do not honor the `getRedisCache: () => mockRedisClient` override you added, because they import the internal function rather than the top-level export. `cache.ts` already binds itself to the real `cacheable` at import time. Today the specific assertions happen to avoid triggering those paths, but the isolation is gone: the first unit-under-test path that calls them will try to reach real infrastructure instead of `mockRedisClient`. Consider keeping explicit stubs for any redis/queue/bullmq-touching exports you don't want to run for real.</violation>
</file>

<file name="packages/ai/src/ai/mcp/conversation-store.test.ts">

<violation number="1" location="packages/ai/src/ai/mcp/conversation-store.test.ts:25">
P2: This mock now derives its whole export surface from a top-level `const redisModule = { ...actualRedis }` that is referenced inside the hoisted `vi.mock("@databuddy/redis")` factory. Vitest hoists `vi.mock` above all imports and runs the factory when the mocked module is first resolved — and this file now also imports `@databuddy/redis` statically at the top, so that resolution can occur before `redisModule` is initialized. That is the exact closure-over-top-level-binding case Vitest documents as causing "Cannot access ... before initialization" errors, and it will break as soon as module resolution timing shifts. Prefer `vi.hoisted` for the shared reference (or `vi.importActual` inside the factory) so the base module is captured safely, and consider restoring explicit stubs for the redis-touching exports so the test stays isolated.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as Browser / Device
    participant Basket as Basket API
    participant Kafka as Redpanda (Kafka)
    participant Vector as Vector (Infra)
    participant ClickHouse as ClickHouse
    participant Redis as Redis (Dedup + BullMQ)
    participant BullMQ as BullMQ Queue (Uptime)
    participant Uptime as Uptime Service
    participant Worker as Uptime Delivery Worker
    participant DB as PostgreSQL

    Note over Client,ClickHouse: Analytics Event Ingestion (Basket)

    Client->>Basket: POST /events, POST /batch, GET /px.jpg
    Basket->>Redis: reserveDuplicate(deliveryId, eventType, sourceEventId)
    alt Duplicate found
        Redis-->>Basket: duplicate: true
        Basket-->>Client: 200 (silent skip)
    else Retryable (owned by another request)
        Redis-->>Basket: retryable: true
        Basket-->>Client: 503 + Retry-After
    else New event or Redis unavailable
        Redis-->>Basket: reservation token
        Basket->>Kafka: send(topic, event) with stable event ID
        alt Kafka succeeds
            Kafka-->>Basket: acknowledged
            Basket->>Redis: markDuplicateReservationDelivered(token)
        else Kafka fails
            Basket->>ClickHouse: direct fallback insert (clickhouseDirectFallbackInsert)
            alt ClickHouse succeeds
                ClickHouse-->>Basket: acknowledged
                Basket->>Redis: markDuplicateReservationDelivered(token)
            else ClickHouse fails
                Basket->>Redis: releaseDuplicateReservation(token)
                Basket-->>Client: 503 + Retry-After
            end
        end
    end

    Note over ClickHouse: Vector reads from Redpanda
    Vector->>Kafka: consume analytics-* topics
    Vector->>ClickHouse: batch insert with acknowledgements enabled
    alt ClickHouse acknowledges
        Vector->>Kafka: commit offset
    else ClickHouse fails
        Vector-->>Vector: retain offset / retry later
    end

    Note over Uptime,DB: Uptime Monitoring Flow
    BullMQ->>Uptime: schedule trigger (uptime-check job)
    Uptime->>DB: lookup schedule
    DB-->>Uptime: schedule + website config
    Uptime->>Uptime: runUptimeCheck() → produce UptimeData with event_id
    Uptime->>Redis: checkpoint: update job data with probe result
    alt Checkpoint fails
        Redis-->>Uptime: error
        Uptime-->>BullMQ: retry job (source level retry)
    else Checkpoint succeeds
        Uptime->>BullMQ: enqueueUptimeDelivery() → add to uptime-event-delivery queue
        alt Enqueue fails
            Redis-->>Uptime: error
            Uptime-->>BullMQ: retry job (source level retry)
        else Enqueue succeeds
            BullMQ->>Worker: deliver uptime-event-delivery job
            Worker->>Kafka: sendUptimeEvent() with acks=-1
            alt Kafka acknowledges
                Kafka-->>Worker: success
                Worker->>Redis: job completed (idempotent via event_id)
            else Kafka fails
                Worker-->>BullMQ: retry (fixed delay, up to 1M attempts)
                BullMQ->>Worker: retry delivery with same event data
            end
            Uptime->>Uptime: fireTransitionAlerts()
        end
    end

    Note over ClickHouse,Worker: Query-Time Dedup for Replayed Uptime
    Worker->>ClickHouse: SELECT ... LIMIT 1 BY [all check fields]
    ClickHouse-->>Worker: deduplicated results (replayed payloads collapsed)

    Note over Client,Basket: Pixel Retry by Tracker SDK
    Client->>Basket: GET /px.jpg (pixel request)
    alt Delivery fails (503)
        Basket-->>Client: 503, no image body, Retry-After: 5
        Client->>Client: retry after capped exponential backoff
        Client->>Basket: same eventId, stable analytics ID
    else Success
        Basket-->>Client: 200, transparent pixel GIF
    end
Loading

Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

if (generation !== deliveryGeneration) {
return { attempts: retry, success: false };
}
if (await load()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Opting out or clearing during an in-flight pixel load still lets that pixel request finish and transmit its event. Track active Image instances and cancel them when cancelPendingRequests runs; generation checks only prevent later retries.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tracker/src/plugins/pixel.ts, line 124:

<comment>Opting out or clearing during an in-flight pixel load still lets that pixel request finish and transmit its event. Track active `Image` instances and cancel them when `cancelPendingRequests` runs; generation checks only prevent later retries.</comment>

<file context>
@@ -76,36 +107,57 @@ export function initPixelTracking(tracker: BaseTracker) {
+				if (generation !== deliveryGeneration) {
+					return { attempts: retry, success: false };
+				}
+				if (await load()) {
+					return { attempts: retry + 1, success: true };
+				}
</file context>

Comment thread apps/basket/src/lib/event-service.ts Outdated

await runPromise(send("analytics-events", trackEvent));
} catch (error) {
await releaseDuplicateReservation(reservation);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Ambiguous delivery retries can inflate core analytics: this releases the guard after a rejection even if ClickHouse/Redpanda accepted the event before the response failed. Stable IDs alone do not deduplicate these ReplicatedMergeTree rows, so preserve an ambiguous reservation or add ID-level deduplication before permitting retry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/basket/src/lib/event-service.ts, line 225:

<comment>Ambiguous delivery retries can inflate core analytics: this releases the guard after a rejection even if ClickHouse/Redpanda accepted the event before the response failed. Stable IDs alone do not deduplicate these `ReplicatedMergeTree` rows, so preserve an ambiguous reservation or add ID-level deduplication before permitting retry.</comment>

<file context>
@@ -130,54 +169,64 @@ export function insertTrackEvent(
+
+			await runPromise(send("analytics-events", trackEvent));
+		} catch (error) {
+			await releaseDuplicateReservation(reservation);
+			throw deliveryUnavailable(error);
+		}
</file context>

Comment on lines +58 to +59
ORDER BY timestamp DESC
LIMIT 1 BY ${UPTIME_REPLAY_IDENTITY}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Dashboard queries sort the entire selected uptime range before deduplication, adding avoidable work to every uptime aggregate and window query. Since the identity covers every stored column, selection order cannot affect the retained row; remove this inner sort and let consumers that need presentation ordering keep their outer ORDER BY.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/query/builders/uptime.ts, line 58:

<comment>Dashboard queries sort the entire selected uptime range before deduplication, adding avoidable work to every uptime aggregate and window query. Since the identity covers every stored column, selection order cannot affect the retained row; remove this inner sort and let consumers that need presentation ordering keep their outer `ORDER BY`.</comment>

<file context>
@@ -18,6 +18,47 @@ import type { SimpleQueryConfig } from "../types";
+		FROM ${UPTIME_TABLE}
+		WHERE
+			${scope}
+		ORDER BY timestamp DESC
+		LIMIT 1 BY ${UPTIME_REPLAY_IDENTITY}
+	)`;
</file context>
Suggested change
ORDER BY timestamp DESC
LIMIT 1 BY ${UPTIME_REPLAY_IDENTITY}
LIMIT 1 BY ${UPTIME_REPLAY_IDENTITY}

Comment thread apps/basket/src/index.ts Outdated
const { shutdownRedis } = await import("@databuddy/redis");
// Wait for acknowledged delivery before tearing down its dependencies.
try {
await runPromise(disconnect);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: A full producer drain now leaves less than two seconds before the hard shutdown timer terminates Redis/Postgres and log flushing. Consider budgeting the process timeout for this serial drain plus cleanup, so a successful near-deadline delivery does not routinely skip the remaining shutdown work.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/basket/src/index.ts, line 89:

<comment>A full producer drain now leaves less than two seconds before the hard shutdown timer terminates Redis/Postgres and log flushing. Consider budgeting the process timeout for this serial drain plus cleanup, so a successful near-deadline delivery does not routinely skip the remaining shutdown work.</comment>

<file context>
@@ -79,13 +84,35 @@ async function gracefulShutdown(signal: string, exitCode = 0) {
 		const { shutdownRedis } = await import("@databuddy/redis");
+		// Wait for acknowledged delivery before tearing down its dependencies.
+		try {
+			await runPromise(disconnect);
+		} catch (error) {
+			finalExitCode = 1;
</file context>

Comment thread packages/rpc/src/routers/status-page.ts Outdated
FROM ${UPTIME_TABLE}
WHERE
${scope}
ORDER BY timestamp DESC

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Public status-page refreshes now sort every selected check before deduplication, although each LIMIT BY key contains every physical uptime_monitor column. Exact duplicates are indistinguishable downstream, so remove this sort to avoid unnecessary sort and memory work for the 90-day query.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/rpc/src/routers/status-page.ts, line 72:

<comment>Public status-page refreshes now sort every selected check before deduplication, although each `LIMIT BY` key contains every physical `uptime_monitor` column. Exact duplicates are indistinguishable downstream, so remove this sort to avoid unnecessary sort and memory work for the 90-day query.</comment>

<file context>
@@ -33,6 +33,46 @@ import {
+		FROM ${UPTIME_TABLE}
+		WHERE
+			${scope}
+		ORDER BY timestamp DESC
+		LIMIT 1 BY ${UPTIME_REPLAY_IDENTITY}
+	)`;
</file context>

Comment thread apps/uptime/src/worker.ts Outdated
timestamp?: unknown;
};
return (
typeof event.event_id === "string" &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Corrupt or legacy persisted delivery jobs containing only these three fields pass validation and are sent to Redpanda as UptimeData. Validate every required UptimeData field (and json_data when present) before enqueue/replay so invalid data fails the job instead.

(Based on your team's feedback about runtime type validation.) .

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/uptime/src/worker.ts, line 448:

<comment>Corrupt or legacy persisted delivery jobs containing only these three fields pass validation and are sent to Redpanda as `UptimeData`. Validate every required `UptimeData` field (and `json_data` when present) before enqueue/replay so invalid data fails the job instead.

(Based on your team's feedback about runtime type validation.) .</comment>

<file context>
@@ -360,30 +416,156 @@ export async function processUptimeCheck(
+		timestamp?: unknown;
+	};
+	return (
+		typeof event.event_id === "string" &&
+		typeof event.site_id === "string" &&
+		typeof event.timestamp === "number"
</file context>

Comment thread packages/ai/src/ai/agents/cache.test.ts Outdated
getLinkCacheKey: vi.fn((slug: string) => `link:${slug}`),
getRateLimitHeaders: vi.fn(() => ({})),
getInsightsQueue: vi.fn(() => ({})),
...redisModule,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Spreading ...redisModule into the mock re-exposes the real Redis-backed implementations that the old per-export stubs deliberately isolated. The internal redis functions (cacheable, the invalidate* helpers, and the redis proxy) call the module's own getRedisCache, which constructs a real ioredis client — they do not honor the getRedisCache: () => mockRedisClient override you added, because they import the internal function rather than the top-level export. cache.ts already binds itself to the real cacheable at import time. Today the specific assertions happen to avoid triggering those paths, but the isolation is gone: the first unit-under-test path that calls them will try to reach real infrastructure instead of mockRedisClient. Consider keeping explicit stubs for any redis/queue/bullmq-touching exports you don't want to run for real.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/agents/cache.test.ts, line 58:

<comment>Spreading `...redisModule` into the mock re-exposes the real Redis-backed implementations that the old per-export stubs deliberately isolated. The internal redis functions (`cacheable`, the `invalidate*` helpers, and the `redis` proxy) call the module's own `getRedisCache`, which constructs a real `ioredis` client — they do not honor the `getRedisCache: () => mockRedisClient` override you added, because they import the internal function rather than the top-level export. `cache.ts` already binds itself to the real `cacheable` at import time. Today the specific assertions happen to avoid triggering those paths, but the isolation is gone: the first unit-under-test path that calls them will try to reach real infrastructure instead of `mockRedisClient`. Consider keeping explicit stubs for any redis/queue/bullmq-touching exports you don't want to run for real.</comment>

<file context>
@@ -44,130 +45,18 @@ const mockEnrichAgentContext = vi.fn(
-	getLinkCacheKey: vi.fn((slug: string) => `link:${slug}`),
-	getRateLimitHeaders: vi.fn(() => ({})),
-	getInsightsQueue: vi.fn(() => ({})),
+	...redisModule,
 	getRedisCache: () => mockRedisClient,
-	getUptimeQueue: vi.fn(() => ({})),
</file context>

}),
};

const redisModule = { ...actualRedis };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This mock now derives its whole export surface from a top-level const redisModule = { ...actualRedis } that is referenced inside the hoisted vi.mock("@databuddy/redis") factory. Vitest hoists vi.mock above all imports and runs the factory when the mocked module is first resolved — and this file now also imports @databuddy/redis statically at the top, so that resolution can occur before redisModule is initialized. That is the exact closure-over-top-level-binding case Vitest documents as causing "Cannot access ... before initialization" errors, and it will break as soon as module resolution timing shifts. Prefer vi.hoisted for the shared reference (or vi.importActual inside the factory) so the base module is captured safely, and consider restoring explicit stubs for the redis-touching exports so the test stays isolated.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/mcp/conversation-store.test.ts, line 25:

<comment>This mock now derives its whole export surface from a top-level `const redisModule = { ...actualRedis }` that is referenced inside the hoisted `vi.mock("@databuddy/redis")` factory. Vitest hoists `vi.mock` above all imports and runs the factory when the mocked module is first resolved — and this file now also imports `@databuddy/redis` statically at the top, so that resolution can occur before `redisModule` is initialized. That is the exact closure-over-top-level-binding case Vitest documents as causing "Cannot access ... before initialization" errors, and it will break as soon as module resolution timing shifts. Prefer `vi.hoisted` for the shared reference (or `vi.importActual` inside the factory) so the base module is captured safely, and consider restoring explicit stubs for the redis-touching exports so the test stays isolated.</comment>

<file context>
@@ -21,129 +22,16 @@ const mockRedisClient = {
 	}),
 };
 
+const redisModule = { ...actualRedis };
+
 vi.mock("@databuddy/redis", () => ({
</file context>

const img = new Image();
img.onload = () => resolve(true);
img.onerror = () => resolve(false);
img.src = url.toString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Every delivery now blocks on the Image onload/onerror promise with no timeout or abort path, and a pixel whose load never settles leaves BaseTracker's queue permanently stuck. Because sendBeacon now returns false unconditionally, the unload/destroy and normal flush paths all go through this api.fetch image-load promise. If a request is aborted by navigation or the browser never fires onload/onerror for it, await load() never resolves, so api.fetch never returns. In _flushQueue, the finally block only clears meta.flushing when the fetch resolves — so this queue stays flushing: true forever and every subsequent event is queued but never sent. The previous sendBeacon path returned true immediately (clearing the queue) and fired the Image in the background, so this hang could not previously brick a queue. Consider wrapping load() in a timeout (e.g. racing against the existing MAX_PIXEL_RETRY_DELAY_MS) and treating a timeout as a failed attempt so the promise always settles.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tracker/src/plugins/pixel.ts, line 116:

<comment>Every delivery now blocks on the Image onload/onerror promise with no timeout or abort path, and a pixel whose load never settles leaves BaseTracker's queue permanently stuck. Because sendBeacon now returns false unconditionally, the unload/destroy and normal flush paths all go through this api.fetch image-load promise. If a request is aborted by navigation or the browser never fires onload/onerror for it, `await load()` never resolves, so `api.fetch` never returns. In `_flushQueue`, the `finally` block only clears `meta.flushing` when the fetch resolves — so this queue stays `flushing: true` forever and every subsequent event is queued but never sent. The previous sendBeacon path returned `true` immediately (clearing the queue) and fired the Image in the background, so this hang could not previously brick a queue. Consider wrapping `load()` in a timeout (e.g. racing against the existing MAX_PIXEL_RETRY_DELAY_MS) and treating a timeout as a failed attempt so the promise always settles.</comment>

<file context>
@@ -76,36 +107,57 @@ export function initPixelTracking(tracker: BaseTracker) {
+				const img = new Image();
+				img.onload = () => resolve(true);
+				img.onerror = () => resolve(false);
+				img.src = url.toString();
+			});
+
</file context>

*/

const UPTIME_TABLE = "uptime.uptime_monitor";
const UPTIME_REPLAY_IDENTITY = [

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This change adds the same 22-column UPTIME_REPLAY_IDENTITY and the same uptimeEventSource() helper in two separate packages (ai and rpc) with no shared source of truth or cross-reference comment. If a column is ever added to the table or the replay identity list is adjusted in one file, the two sites will silently deduplicate differently (query-time anti-replay behavior diverges), and the divergence won't be caught. Since the two packages can't easily share a module, at minimum add a cross-reference comment on each definition pointing at the other location (and at the schema file) so future edits prompt checking the paired copy.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/query/builders/uptime.ts, line 21:

<comment>This change adds the same 22-column `UPTIME_REPLAY_IDENTITY` and the same `uptimeEventSource()` helper in two separate packages (ai and rpc) with no shared source of truth or cross-reference comment. If a column is ever added to the table or the replay identity list is adjusted in one file, the two sites will silently deduplicate differently (query-time anti-replay behavior diverges), and the divergence won't be caught. Since the two packages can't easily share a module, at minimum add a cross-reference comment on each definition pointing at the other location (and at the schema file) so future edits prompt checking the paired copy.</comment>

<file context>
@@ -18,6 +18,47 @@ import type { SimpleQueryConfig } from "../types";
  */
 
 const UPTIME_TABLE = "uptime.uptime_monitor";
+const UPTIME_REPLAY_IDENTITY = [
+	"site_id",
+	"url",
</file context>

@izadoesdev

Copy link
Copy Markdown
Member Author

Superseded by #585, rebuilt cleanly against current staging.

@izadoesdev izadoesdev closed this Aug 2, 2026
@izadoesdev
izadoesdev deleted the codex/reliability-hardening branch August 2, 2026 09:29
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