Skip to content

track drop land#1381

Open
Gianfranco99 wants to merge 6 commits intomainfrom
feat/track_drop_metrics
Open

track drop land#1381
Gianfranco99 wants to merge 6 commits intomainfrom
feat/track_drop_metrics

Conversation

@Gianfranco99
Copy link
Copy Markdown
Collaborator

@Gianfranco99 Gianfranco99 commented Dec 16, 2025

Add drop land dashboard with metrics tracking

This PR adds a new dashboard for tracking drop land metrics, including:

  • Backend API endpoints for querying drop land data with metrics
  • New UI components for visualizing drop performance
  • Support for filtering by location, date range, and fee rates
  • Global metrics aggregation for all drops owned by a reinjector

The implementation includes:

  • New repository methods for calculating protocol fees, influenced auctions, and stake distributions
  • Token price conversion for USD-based ROI calculations
  • Detailed per-token breakdowns of fees, inflows, and distributions
  • Privacy protection for the drop dashboard via bypass token authentication

The dashboard provides comprehensive analytics for drop lands, helping users track performance and ROI across their reinjections.

Copy link
Copy Markdown
Collaborator Author

Gianfranco99 commented Dec 16, 2025


How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • Merge queue - adds this PR to the back of the merge queue
  • Hotfix 🔥 - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@Gianfranco99 Gianfranco99 marked this pull request as ready for review December 16, 2025 21:10
@Gianfranco99 Gianfranco99 requested review from a team as code owners December 16, 2025 21:10
Comment on lines +112 to +114
if (options?.endOfDay) {
date.setUTCHours(23, 59, 59, 0);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The end-of-day timestamp is incomplete and will miss events in the last 999 milliseconds of the day. Line 113 sets milliseconds to 0 instead of 999, meaning until dates will exclude events between 23:59:59.001 and 23:59:59.999.

if (options?.endOfDay) {
  date.setUTCHours(23, 59, 59, 999);
}
Suggested change
if (options?.endOfDay) {
date.setUTCHours(23, 59, 59, 0);
}
if (options?.endOfDay) {
date.setUTCHours(23, 59, 59, 999);
}

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +133 to +143
let since = query.since.as_ref().and_then(|s| {
DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.with_timezone(&Utc).naive_utc())
});

let until = query.until.as_ref().and_then(|u| {
DateTime::parse_from_rfc3339(u)
.ok()
.map(|dt| dt.with_timezone(&Utc).naive_utc())
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Timestamp parsing inconsistency causes different error handling behavior between endpoints. The get_drop_lands endpoint uses .and_then() which silently ignores invalid timestamp formats (treats them as None), while get_global_metrics (lines 232-240) returns BAD_REQUEST for the same scenario. This inconsistency will confuse API consumers.

// Change from and_then to map for consistent error handling
let since = query.since.as_ref().map(|s| {
    DateTime::parse_from_rfc3339(s)
        .ok()
        .map(|dt| dt.with_timezone(&Utc).naive_utc())
});

// Then add validation like in get_global_metrics
let since = match since {
    Some(Some(dt)) => Some(dt),
    Some(None) => return Err(axum::http::StatusCode::BAD_REQUEST),
    None => None,
};
Suggested change
let since = query.since.as_ref().and_then(|s| {
DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.with_timezone(&Utc).naive_utc())
});
let until = query.until.as_ref().and_then(|u| {
DateTime::parse_from_rfc3339(u)
.ok()
.map(|dt| dt.with_timezone(&Utc).naive_utc())
});
let since = query.since.as_ref().map(|s| {
DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.with_timezone(&Utc).naive_utc())
});
let since = match since {
Some(Some(dt)) => Some(dt),
Some(None) => return Err(axum::http::StatusCode::BAD_REQUEST),
None => None,
};
let until = query.until.as_ref().map(|u| {
DateTime::parse_from_rfc3339(u)
.ok()
.map(|dt| dt.with_timezone(&Utc).naive_utc())
});
let until = match until {
Some(Some(dt)) => Some(dt),
Some(None) => return Err(axum::http::StatusCode::BAD_REQUEST),
None => None,
};

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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.

This is valid

@github-actions
Copy link
Copy Markdown

github-actions bot commented Dec 17, 2025

Deployment informations

This PR has passed automatic testing, and is ready to be tested manually.

Warning

You might need to have a logged-in employee runelabs account to access the deployment environment.

environment status url
mainnet https://1b9cbcf-ponzi-land-mainnet.runelabs.workers.dev/
sepolia https://1b9cbcf-ponzi-land-sepolia.runelabs.workers.dev/
mainnet-test https://1b9cbcf-ponzi-land-mainnet-test.runelabs.workers.dev/
next (staging) https://1b9cbcf-ponzi-land-next.runelabs.workers.dev/

This comment was written by a bot!

Copy link
Copy Markdown
Contributor

@knownasred knownasred left a comment

Choose a reason for hiding this comment

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

What is this. Please take a bit of time to read the comments, and address them before going further on this.

Comment on lines +118 to +124
if (
hasBypassToken &&
pathname.startsWith('/dashboard/drops') &&
!hasValidBypass
) {
return redirect(302, '/maintenance');
}
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.

If you wanted the drops dashboard to be private, then it would have been best to move it to xperience or another private repo. People can and will run this locally

Comment on lines +57 to +90
function parseDateValue(value: string): DateParts | null {
if (!value) return null;

const isoMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoMatch) {
return {
year: Number(isoMatch[1]),
month: Number(isoMatch[2]),
day: Number(isoMatch[3]),
};
}

const slashMatch = value.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
if (slashMatch) {
const first = Number(slashMatch[1]);
const second = Number(slashMatch[2]);
const year = Number(slashMatch[3]);

let month = first;
let day = second;
if (first > 12 && second <= 12) {
// First number cannot be month -> interpret as day/month
day = first;
month = second;
} else if (second > 12 && first <= 12) {
// Second number cannot be month -> keep default month/day order
day = second;
month = first;
}

if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
return { year, month, day };
}
}
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.

Avoid doing custom date parsing if possible. I don't see why it would be required in this specific case, as it is pretty much standard parsing


async function parseResponse<T>(response: Response) {
if (!response.ok) {
throw new Error('Ocurrió un error al cargar las drops');
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.

Errors should be in english.

Comment on lines +190 to +195
async function parseResponse<T>(response: Response) {
if (!response.ok) {
throw new Error('Ocurrió un error al cargar las drops');
}
return (await response.json()) as T;
}
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.

Also, not sure it is useful, as you can type the return variable to the same effect, making things a bit easier to understand

Comment on lines +47 to +49
const DEFAULT_LEVEL = 1;
const DEFAULT_FEE_RATE = 900_000;
const DEFAULT_SALE_FEE_RATE = 500_000;
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.

Please centralize them somewhere as game params, as I'm starting to see a lot of consts everywhere for a lot of logic

Comment on lines +232 to +236
let since = match since {
Some(Some(dt)) => Some(dt),
Some(None) => return Err(axum::http::StatusCode::BAD_REQUEST),
None => None,
};
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.

You can treat a Result instead? Why go through a map via .ok() ?

.map_err(|_| axum::http::StatusCode::INTERNAL_SERVER_ERROR)?;

// Sum area protocol fees across all tokens
use sqlx::types::BigDecimal;
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.

ohno

Comment on lines +258 to +272
let total_revenue_bd: BigDecimal = metrics
.area_fees_per_token
.0
.values()
.fold(zero_bd.clone(), |acc, v| acc + BigDecimal::from(*v));
let total_inflows_bd: BigDecimal = metrics
.inflows_per_token
.0
.values()
.fold(zero_bd.clone(), |acc, v| acc + BigDecimal::from(*v));
let total_sale_fees_bd: BigDecimal = metrics
.sale_fees_per_token
.0
.values()
.fold(zero_bd.clone(), |acc, v| acc + BigDecimal::from(*v));
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.

Sum through the U256 add

Comment on lines +274 to +296
// Calculate ROI
let distributed_bd = BigDecimal::from(metrics.total_distributed);
let global_roi = if distributed_bd > zero_bd {
let ratio = &total_revenue_bd / &distributed_bd;
ratio.to_string().parse::<f64>().unwrap_or(0.0)
} else {
0.0
};

// Build per-token aggregates so the client can apply decimals/prices per token
let mut token_keys = std::collections::HashSet::new();
for k in metrics.distributed_per_token.0.keys() {
token_keys.insert(k.clone());
}
for k in metrics.area_fees_per_token.0.keys() {
token_keys.insert(k.clone());
}
for k in metrics.inflows_per_token.0.keys() {
token_keys.insert(k.clone());
}
for k in metrics.sale_fees_per_token.0.keys() {
token_keys.insert(k.clone());
}
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.

All of these seems to be logic that should go into a service. the API should only call the service, and do minimal parsing + re-formatting into the final format

Comment on lines +309 to +338
.map(|token| GlobalTokenMetricsResponse {
token: token.clone(),
distributed: metrics
.distributed_per_token
.0
.get(&token)
.cloned()
.unwrap_or_else(|| sqlx::types::BigDecimal::from(0i32).into())
.to_string(),
fees: metrics
.area_fees_per_token
.0
.get(&token)
.cloned()
.unwrap_or_else(|| sqlx::types::BigDecimal::from(0i32).into())
.to_string(),
sale_fees: metrics
.sale_fees_per_token
.0
.get(&token)
.cloned()
.unwrap_or_else(|| sqlx::types::BigDecimal::from(0i32).into())
.to_string(),
inflows: metrics
.inflows_per_token
.0
.get(&token)
.cloned()
.unwrap_or_else(|| sqlx::types::BigDecimal::from(0i32).into())
.to_string(),
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.

No logic inside of a struct declaration, especially this much

@0xMugen 0xMugen force-pushed the feat/track_drop_metrics branch from 757fb6b to 75bbf85 Compare December 20, 2025 17:19
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