Skip to content

✨ server: add sliding-window event evaluation#915

Open
mainqueg wants to merge 1 commit intomainfrom
window-rule
Open

✨ server: add sliding-window event evaluation#915
mainqueg wants to merge 1 commit intomainfrom
window-rule

Conversation

@mainqueg
Copy link
Copy Markdown
Member

@mainqueg mainqueg commented Mar 26, 2026

Summary by CodeRabbit

  • New Features
    • Added sliding-window event evaluation with time-windowed aggregation, deduplication, threshold triggers, scheduled expirations, throttled checks, idempotent triggers, automatic retries, and hooks for trigger/expire handling; exposes APIs to report events, read partition state, and stop processing.
  • Tests
    • Added extensive integration tests covering reporting, expiration, check logic, concurrency, retries, idempotency, serialization safety, and error/reporting scenarios.
  • Chores
    • Added release-note entry for a patch release mentioning sliding-window event evaluation.

Open with Devin

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 26, 2026

🦋 Changeset detected

Latest commit: 0f204ad

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@exactly/server Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a new Redis + BullMQ sliding-window evaluation utility (windowRule), an extensive Vitest integration test suite for it, and a changeset declaring a patch release.

Changes

Cohort / File(s) Summary
Changeset metadata
\.changeset/bright-owls-slide.md
New changeset declaring a patch release with release note: "add sliding-window event evaluation".
Windowing implementation
server/utils/windowRule.ts
New windowRule factory (default export): creates a BullMQ queue/worker handling report/expire/check jobs, persists events in per-partition Redis ZSETs, schedules deduped delayed expire jobs, throttles checks, maintains a triggered set, calls evaluate/onTrigger/onTriggerExpire/onEventExpire, instruments Sentry, and exposes read, report, and stop.
Integration tests
server/test/utils/windowRule.test.ts
New comprehensive Vitest suite covering report idempotency, expire scheduling and execution, trigger transition logic, handler and Redis retry semantics, concurrency/race scenarios, deduplication/throttling behavior, serialization/corruption cases, stress tests, and Sentry error capture assertions.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Queue as BullMQ Queue
    participant Worker as BullMQ Worker
    participant Redis
    participant Evaluator as Evaluator
    participant Sentry

    Client->>Queue: enqueue report(event, timestamp)
    Queue->>Worker: process report job
    Worker->>Redis: ZADD partition ZSET (member, score=timestamp)
    Worker->>Queue: schedule expire job (deterministic jobId, delayed)
    Worker->>Queue: enqueue throttled check job
    Worker->>Sentry: start span / add breadcrumb

    Queue->>Worker: process check job
    Worker->>Redis: ZRANGEBYSCORE (now-window .. now)
    Worker->>Redis: SISMEMBER triggered-set
    Worker->>Evaluator: evaluate(deserialized events)
    Evaluator-->>Worker: result { trigger: bool }

    alt trigger transitioned true
        Worker->>Client: call onTrigger(partition, result)
        Worker->>Redis: SADD triggered-set
    else trigger transitioned true->false
        Worker->>Client: call onTriggerExpire(partition)
        Worker->>Redis: SREM triggered-set
    end
    Worker->>Sentry: finish span / add breadcrumb

    Queue->>Worker: process expire job
    Worker->>Redis: ZREM partition ZSET member
    Worker->>Client: call onEventExpire(partition, event) (if deserializable)
    Worker->>Queue: enqueue check job
    Worker->>Sentry: span / breadcrumb
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding a sliding-window event evaluation feature to the server.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch window-rule

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 and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a robust and scalable sliding-window event evaluation system. It allows for defining rules that track events over a specified time period, automatically expiring old events, and triggering actions when certain thresholds are met. The system leverages Redis for efficient data storage and BullMQ for reliable asynchronous processing, ensuring that event evaluation is performed consistently and with proper error handling.

Highlights

  • New Utility: Sliding-Window Event Evaluation: Implemented a createWindowRule utility for managing events within a defined time window, enabling threshold-based triggering and automatic event expiration.
  • Asynchronous Processing with BullMQ: Leveraged BullMQ for robust asynchronous job processing, handling event reporting, expiration, and evaluation checks efficiently.
  • Redis Integration for Event Storage: Utilized Redis sorted sets (ZSETs) to store events, allowing for efficient querying of events within the sliding window.
  • Comprehensive Test Coverage: Added extensive unit tests for the windowRule utility, covering various scenarios including concurrency, deduplication, and error handling.
  • Sentry Error Monitoring: Integrated Sentry to capture and report validation issues and other errors occurring during job processing within the event evaluation system.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@sentry
Copy link
Copy Markdown

sentry bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 93.38843% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.01%. Comparing base (e6efb1b) to head (0f204ad).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
server/utils/windowRule.ts 93.38% 1 Missing and 7 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #915      +/-   ##
==========================================
+ Coverage   71.69%   72.01%   +0.31%     
==========================================
  Files         228      229       +1     
  Lines        8277     8400     +123     
  Branches     2661     2695      +34     
==========================================
+ Hits         5934     6049     +115     
- Misses       2113     2114       +1     
- Partials      230      237       +7     
Flag Coverage Δ
e2e 71.98% <93.38%> (+0.29%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

gemini-code-assist[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@mainqueg mainqueg force-pushed the window-rule branch 2 times, most recently from 1270af3 to bf6bbe1 Compare March 26, 2026 20:49
@mainqueg mainqueg marked this pull request as ready for review March 27, 2026 12:54
chatgpt-codex-connector[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +34 to +40
const jobOptions = {
attempts: 5,
backoff: { delay: config.backoffDelay ?? 1000, type: "exponential" as const },
removeOnComplete: true,
};

const queue = new Queue(queueName, { connection: redis, defaultJobOptions: jobOptions });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 jobOptions is a single-use extracted variable

The jobOptions constant at server/utils/windowRule.ts:34-38 is used only once at line 40 as defaultJobOptions. AGENTS.md's extraction rule says "single-use = inline." This could be inlined into the Queue constructor. However, the object is moderately complex (3 properties with a nested backoff config), and this is a borderline case given the project's "maximum compactness" formatting rule would let prettier handle the line breaking. Flagging for reviewer awareness rather than as a definitive violation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

@mainqueg mainqueg force-pushed the window-rule branch 2 times, most recently from 973de02 to f478f78 Compare March 30, 2026 17:06
chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f204ad397

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +70 to +72
if (now - timestamp > window) {
await redis.zrem(getKey(partition), member);
break;
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 Badge Re-evaluate trigger state after stale report cleanup

In the stale-report branch, the worker removes the member and exits without scheduling check, which leaves wr:<name>:triggered stale when a prior partial attempt inserted that member and set the partition as triggered. This can happen if zadd succeeded but another promise in the same Promise.all failed and the retry runs after the window; the orphan is removed here, but the trigger flag is never recomputed, so the next above-threshold event can be treated as already-triggered and skip onTrigger. After zrem in this path, schedule a check for the partition.

Useful? React with 👍 / 👎.

Comment on lines +166 to +168
if (result.trigger) {
if (onTrigger) await onTrigger(partition, result);
await redis.sadd(triggeredKey, partition);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: The onTrigger callback is called before its state is persisted to Redis. If the Redis write fails, the job retries and calls the callback again, leading to multiple executions.
Severity: MEDIUM

Suggested Fix

To ensure the callback is only fired once, the state should be committed to Redis before the callback is executed. For example, move the await redis.sadd(triggeredKey, partition) call to before the await onTrigger(partition, result) call.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: server/utils/windowRule.ts#L166-L168

Potential issue: The `onTrigger` and `onTriggerExpire` callbacks are executed before
their corresponding state change is committed to Redis. For example, `onTrigger` is
called before `redis.sadd`. If the Redis operation fails due to a transient issue, the
job will retry. On retry, the system checks the state from Redis, finds that the trigger
has not been recorded, and executes the callback a second time. This can lead to
duplicate notifications or actions for a single trigger event. The issue is confirmed by
tests that explicitly expect the callbacks to be called twice upon Redis failure.

Did we get this right? 👍 / 👎 to inform future reviews.

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