Skip to content

feat: Report served-request usage - #19

Open
gagantrivedi wants to merge 5 commits into
feat/proxy-config-discoveryfrom
feat/usage-tracking
Open

feat: Report served-request usage#19
gagantrivedi wants to merge 5 commits into
feat/proxy-config-discoveryfrom
feat/usage-tracking

Conversation

@gagantrivedi

Copy link
Copy Markdown
Member

Changes

Contributes to Flagsmith/flagsmith-private#256

The proxy counts every SDK request it serves — aggregated per environment and resource, keyed by the canonical client key inside resolve_key, so an entry point can't forget to count and unresolved keys never grow the map — 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. A rejected (4xx) chunk is dropped — retrying can't heal a rejection; a failed (5xx/network) chunk is kept for the next flush.
  • Document fetches carry X-Proxy-Key so core stops counting the proxy's own polls.
  • Statically configured environments keep their old billing end to end: their fetches are unmarked, their served requests unreported.
  • 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.

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

How did you test this code?

92 tests (cargo test), 9 new wiremock contract tests in tests/test_usage_tracking.rs: aggregation across client/server keys through the full router; unresolved keys never counted; failed flush merges into the next (nothing lost, nothing doubled); rejected flush drops instead of retrying; 1001 environments chunk as 1000+1; static environment neither counted nor marked; flush inert without a proxy key. clippy + fmt clean.

Counts every request whose environment key resolves, aggregated per
environment and resource, and flushes them to POST /proxy/usage/ every
usage_flush_interval_seconds (default 60). Document fetches now carry
X-Proxy-Key so core can exclude the proxy's own polls from API usage.

Active only when proxy_key is configured; static config-file mode is
unchanged. A final partial window is lost on shutdown (no graceful
shutdown hook yet) — usage metering tolerates that.
A failed flush used to retry the identical batch forever, so any
permanent rejection — a batch over the server's 1000-row cap, a
revoked proxy key — silently ended usage reporting until restart.
Flushes are now chunked to the server's cap and a rejected (4xx) chunk
is dropped; only network errors and 5xx keep rows for the next flush.

Also per review: a statically configured environment keeps its old
billing end to end (its document fetch is unmarked and its served
requests are not reported); counting moved into resolve_key(resource)
so an SDK entry point cannot forget it; UsageCount renamed UsageRow;
poll_usage renamed flush_usage_periodically; a zero flush interval now
fails config validation instead of silently killing the task.
@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change adds configurable usage flush intervals and public usage types. EnvironmentService records dynamic-environment requests by client key and resource, excludes static environments, and applies proxy authentication to dynamic document requests. It batches usage submissions, retains retryable failures, drops client-error batches, and flushes periodically. Unit and integration tests cover aggregation, serialisation, request tracking, retries, batching, static environments, and proxy-key headers.

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

Merge Risk: 🟡 Moderate · up to 36d7c

Usage totals can be inaccurate, retry behavior can pressure ingestion, and a cross-origin pagination response can receive the proxy credential. These material issues should be resolved 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
gagantrivedi marked this pull request as ready for review August 29, 2026 10:11
@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.

.resolve(environment_key)
.ok_or_else(|| EdgeProxyError::FlagsmithUnknownKey(environment_key.to_string()))
.ok_or_else(|| EdgeProxyError::FlagsmithUnknownKey(environment_key.to_string()))?;
self.track_usage(&keys.client_key, resource);

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 | 🟡 Minor | ⚡ Quick win

Count usage only after a successful endpoint response.

resolve_key increments usage before get_flags_response_data, get_identity_response_data, or get_environment_bytes complete. A resolved key can still return ServiceUnavailable, and the flags path can return FeatureNotFound. These failed responses are counted as served requests. Move tracking to each successful Ok path, including cache hits.

// unmarked and keep their old billing.
if !self.environments.is_static(server_side_key) {
if let Some(proxy_key) = &self.settings.proxy_key {
request = request.header("X-Proxy-Key", proxy_key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
sed -n '200,315p' src/services/environment.rs
printf '\n-- pagination symbols --\n'
rg -n -C 4 'parse_next_link|next_url|Link|X-Proxy-Key|fetch_environment' src/services/environment.rs

Repository: Flagsmith/edge-proxy-rs

Length of output: 10695


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '703,760p' src/services/environment.rs
printf '\n-- client dependency declarations --\n'
rg -n -C 3 'reqwest|^tokio|redirect' Cargo.toml Cargo.lock

Repository: Flagsmith/edge-proxy-rs

Length of output: 3584


Sensitive Data Exposure (CWE-200): Exposure of Sensitive Information to an Unauthorized Actor

Reachability: External · Exploitability: Difficult

Restrict pagination URLs before sending X-Proxy-Key.

Accept only same-origin pagination URLs before forwarding the proxy credential. An absolute rel=next URL from the upstream Link header can otherwise direct the credential-bearing request to another origin.

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.

🗄️ 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.

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.

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