Skip to content

feat: Report served-request usage - #19

Open
gagantrivedi wants to merge 22 commits into
mainfrom
feat/usage-tracking
Open

gagantrivedi wants to merge 22 commits into
mainfrom
feat/usage-tracking

Conversation

@gagantrivedi

@gagantrivedi gagantrivedi commented Aug 29, 2026

Copy link
Copy Markdown
Member

Changes

Contributes to Flagsmith/flagsmith-private#256

The proxy counts every SDK request it serves with a 2xx — a router middleware counts after the handler answers, aggregated per environment and resource under the canonical client key, so failed requests (404, 503) and unresolved keys are never billed — and flushes to POST {api_url}/proxy/usage/ every usage_flush_interval_seconds (default 60), authenticated by the proxy key.

  • Flushes are chunked to the server's 1000-row cap, each chunk a batch with its own Idempotency-Key. A rejected (4xx) batch is dropped — retrying can't heal a rejection. A failed (5xx/network) batch may already have been processed, so it is kept intact and resent unchanged under the same key (Flagsmith/flagsmith-private#286 dedups it); nothing new is drained while a batch is pending, counts keep aggregating in the bounded map.
  • Document fetches carry X-Proxy-Key so core stops counting the proxy's own polls. A rel=next pagination link off api_url's origin now fails the fetch instead of being followed, so neither key can be sent elsewhere.
  • With a proxy key configured, every served environment is billed the same way, whether it came from the config file or the proxy config: fetches are marked, served requests reported, and the server's grant check decides what counts. Without a proxy key nothing is marked or reported.
  • Active only when proxy_key is set; static config-file mode is byte-identical. The final partial window is lost on shutdown (no graceful-shutdown hook) — usage metering tolerates that.

Companion PRs: usage ingestion endpoint (flagsmith-private) and the core middleware exclusion (links in the first comment).

How did you test this code?

85 tests (cargo test), 10 new wiremock contract tests in tests/test_usage_tracking.rs: aggregation across client/server keys through the full router; unresolved keys never counted; failed requests (404, 503) never counted; failed flush resends the same batch under the same key before new counts; rejected flush drops instead of retrying; 1001 environments chunk as 1000+1; static environment billed like a discovered one once a key is set; flush inert without a proxy key. clippy + fmt clean.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: bdef33ab-8c25-4258-9f42-afc9fa3bfeeb

📝 Walkthrough

Walkthrough

The proxy now validates and stores a usage flush interval, aggregates successful requests by client key and resource, and submits usage batches with retry and idempotency handling. Router state now contains environment and usage services. Middleware records supported successful requests. Environment document fetches include the proxy key when configured, and cross-origin pagination links are rejected. Unit and integration tests cover the new behaviour.

Priority: ➖ Normal

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

Merge Risk: 🟡 Moderate · up to 1bfec

Proxy credentials can be exposed by accepted HTTP configurations or redirects, and concurrent configuration refreshes can produce incorrect usage records. These issues should be fixed before merge.


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.

@gagantrivedi

Copy link
Copy Markdown
Member Author

Companion PRs for Flagsmith/flagsmith-private#256:

Merge/deploy order: #282 and 8407 (with the flagsmith-private pin bump) before any proxy build with #19 is deployed, else document polls double-count during the overlap.

@gagantrivedi

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

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

@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

@gagantrivedi I will review the changes.

⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

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

@matthewelwell

Copy link
Copy Markdown

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: beca0e8d-d764-4d52-aa88-31c6fd00bca0

📥 Commits

Reviewing files that changed from the base of the PR and between 90f27dd and 36d7cdd.

📒 Files selected for processing (7)
  • src/config/settings.rs
  • src/environments.rs
  • src/lib.rs
  • src/main.rs
  • src/services/environment.rs
  • src/usage.rs
  • tests/test_usage_tracking.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread src/services/environment.rs Outdated
Comment thread src/services/environment.rs Outdated
Comment thread src/services/environment.rs Outdated
Comment on lines +634 to +640
self.usage.merge(chunk);
all_success = false;
}
Err(e) => {
error!("Failed to report usage: {}", e);
self.usage.merge(chunk);
all_success = false;

@coderabbitai coderabbitai Bot Sep 4, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make retried usage batches idempotent.

Lines 634 and 639 retry a batch after a 5xx response or network error. Neither outcome proves that the ingestion endpoint did not persist the batch. UsageRow has no delivery identifier, and merge combines the failed rows with later usage. A retry can therefore over-count usage. Retain the exact batch with a stable idempotency key that the ingestion endpoint recognises before retrying it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Interesting, this could be a problem too. If the ingestion endpoint fails after persisting we could end up double counting.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@matthewelwell, agreed. A timeout, connection failure, or 5xx response is ambiguous. The ingestion endpoint can persist the batch before the proxy observes the failure.

The proxy should retain the exact batch and resend it with the same stable idempotency key. The ingestion endpoint should persist that key atomically and treat later deliveries of that key as successful no-ops. The key must remain stable across retries and must not be derived only from the usage rows.

🐇🔍

You are interacting with an AI system.

Comment thread src/services/environment.rs Outdated
Comment on lines +652 to +654
let mut interval = tokio::time::interval(Duration::from_secs(
self.settings.usage_flush_interval_seconds,
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -A2 '^name = "tokio"$' Cargo.lock

Repository: Flagsmith/edge-proxy-rs

Length of output: 275


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '600,690p' src/services/environment.rs
rg -n "flush_usage|MissedTickBehavior|interval\\(" src Cargo.toml Cargo.lock

Repository: Flagsmith/edge-proxy-rs

Length of output: 4058


🌐 Web query:

Tokio 1.48.0 interval missed tick behavior default Burst set_missed_tick_behavior official docs

💡 Result:

In Tokio 1.48.0, the default behavior for an Interval when a tick is missed is MissedTickBehavior::Burst [1][2]. When the Burst strategy is active, the Interval will fire ticks as quickly as possible until it has "caught up" to the expected schedule [1][3]. This ensures that the ticks produced correspond to the same instants that would have been yielded if no ticks had been missed [1][3]. You can configure this behavior using the set_missed_tick_behavior method on an Interval instance [4][5]: interval.set_missed_tick_behavior(MissedTickBehavior::Skip); The MissedTickBehavior enum provides three strategies: - Burst (Default): Fires ticks as fast as possible to catch up [1][3]. - Skip: Skips missed ticks and schedules the next tick at the next multiple of the period [3]. - Delay: Reschedules all future ticks relative to the time the delayed tick was actually called [3]. The Burst strategy is the default to maintain backwards compatibility and because it is the desired behavior for most use cases [1][6]. Note that these strategies apply specifically when the delay exceeds 5 milliseconds due to executor precision limitations [1].

Citations:


Use a non-burst missed-tick policy.

Tokio 1.48.0 uses MissedTickBehavior::Burst by default. If flush_usage exceeds the configured interval, subsequent tick() calls can complete immediately and retry failed reports without the configured delay. Set MissedTickBehavior::Delay or Skip.

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

Mostly just agreeing with the CR comments with one additional recommendation for desloppification.

Comment thread src/services/environment.rs Outdated
Comment thread src/services/environment.rs Outdated
Comment on lines +597 to +602
/// Report the counts accumulated since the last flush to the usage
/// endpoint, in chunks the server accepts. A rejected (4xx) chunk is
/// dropped — retrying cannot heal a rejection, and losing one window
/// beats resending a poisoned batch forever. Any other failure keeps
/// the chunk for the next flush. Returns false when any chunk was
/// not accepted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't think this docstring really adds anything - the code describes all of this already

Comment thread src/services/environment.rs Outdated
Comment on lines +634 to +640
self.usage.merge(chunk);
all_success = false;
}
Err(e) => {
error!("Failed to report usage: {}", e);
self.usage.merge(chunk);
all_success = false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Interesting, this could be a problem too. If the ingestion endpoint fails after persisting we could end up double counting.

@gagantrivedi
gagantrivedi force-pushed the feat/proxy-config-discovery branch from 90f27dd to 43c1d37 Compare September 5, 2026 10:13
@gagantrivedi
gagantrivedi changed the base branch from feat/proxy-config-discovery to feat/proxy-config-sync September 19, 2026 08:00
The proxy counts every SDK request it serves, aggregated per environment
and resource inside resolve_key so no entry point can forget to count,
and flushes to POST {api_url}/proxy/usage/ every
usage_flush_interval_seconds (default 60) with the proxy key.

- flushes are chunked to the server's 1000-row cap; a rejected (4xx)
  chunk is dropped, a failed (5xx/network) chunk is kept for next time
- document fetches carry X-Proxy-Key so core stops counting the proxy's
  own polls
- statically configured environments keep their old billing: fetches
  unmarked, served requests unreported
- inert without proxy_key; the final partial window is lost on shutdown
Counting inside resolve_key billed requests that then failed (404 on an
unknown feature, 503 before the document loaded). A router middleware now
counts after the handler answers, and only when it answered 2xx.
The next page request carries the environment and proxy keys, so a Link
header must not be able to send them off api_url's origin.
A batch that failed with 5xx or a network error may still have been
processed. It is now kept intact and resent with the same
Idempotency-Key so the server can recognise it, instead of being merged
into the next flush and counted twice. Nothing new is drained while a
batch is pending; counts keep aggregating in the bounded map.
tokio intervals burst missed ticks by default, so a flush that overran
the interval would be followed by back-to-back flushes.
@gagantrivedi
gagantrivedi changed the base branch from feat/proxy-config-sync to main September 19, 2026 09:01
Static and discovered environments were billed differently: static ones
kept unmarked fetches and unreported requests. With a proxy key, all
fetches are now marked and all served requests reported; the server's
grant check decides what counts.
3e1d7f2 swept criterion and a [[bench]] target into an unrelated commit;
the bench file itself was never added.
EnvironmentService was also counting served requests and posting usage
batches. That now lives in services/usage.rs; the router state is a
struct holding both services, and the middleware resolves the key
through one and records on the other.
@gagantrivedi

Copy link
Copy Markdown
Member Author

@coderabbitai full review

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟠 Major · Require HTTPS when proxy_key is configured. · settings.rs:100-104

src/config/settings.rs:100-104
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Require HTTPS when proxy_key is configured.

api_url accepts non-HTTPS URLs, while the usage, configuration, and document requests send X-Proxy-Key to that URL. Reject a non-HTTPS api_url when proxy_key is set before starting credential-bearing reporting.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: e8164de0-9861-474e-9ea4-fdd676e17ec5

📥 Commits

Reviewing files that changed from the base of the PR and between 36d7cdd and 1bfece5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (21)
  • Cargo.toml
  • src/config/settings.rs
  • src/lib.rs
  • src/main.rs
  • src/middleware/cors.rs
  • src/middleware/mod.rs
  • src/middleware/usage.rs
  • src/routes/environment_document.rs
  • src/routes/flags.rs
  • src/routes/health.rs
  • src/routes/identities.rs
  • src/routes/mod.rs
  • src/services/environment.rs
  • src/services/mod.rs
  • src/services/usage.rs
  • src/state.rs
  • src/usage.rs
  • tests/test_cors.rs
  • tests/test_health.rs
  • tests/test_server.rs
  • tests/test_usage_tracking.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread src/middleware/usage.rs
Comment thread src/services/usage.rs
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 51 minutes.

reqwest strips only its own list of sensitive headers on a cross-host
redirect; X-Proxy-Key and X-Environment-Key would be forwarded.
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.

2 participants