Skip to content

Commit

Permalink
Add in-client views counter to stats pages
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenwardy committed Oct 22, 2024
1 parent 2ff11de commit 8ecc856
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 7 deletions.
11 changes: 10 additions & 1 deletion app/blueprints/api/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from app.markdown import render_markdown
from app.models import Tag, PackageState, PackageType, Package, db, PackageRelease, Permission, \
MinetestRelease, APIToken, PackageScreenshot, License, ContentWarning, User, PackageReview, Thread, Collection, \
PackageAlias, Language
PackageAlias, Language, PackageDailyStats
from app.querybuilder import QueryBuilder
from app.utils import is_package_page, get_int_or_abort, url_set_query, abs_url, is_yes, get_request_date, cached, \
cors_allowed
Expand All @@ -39,6 +39,7 @@
from .auth import is_api_authd
from .support import error, api_create_vcs_release, api_create_zip_release, api_create_screenshot, \
api_order_screenshots, api_edit_package, api_set_cover_image
from ...rediscache import make_view_key, set_temp_key, has_key


@bp.route("/api/packages/")
Expand Down Expand Up @@ -99,6 +100,14 @@ def package_view(package):
@is_package_page
@cors_allowed
def package_view_client(package: Package):
ip = request.headers.get("X-Forwarded-For") or request.remote_addr
if ip is not None and (request.headers.get("User-Agent") or "").startswith("Minetest"):
key = make_view_key(ip, package)
if not has_key(key):
set_temp_key(key, "true")
PackageDailyStats.notify_view(package)
db.session.commit()

protocol_version = request.args.get("protocol_version")
engine_version = request.args.get("engine_version")
if protocol_version or engine_version:
Expand Down
2 changes: 1 addition & 1 deletion app/blueprints/packages/releases.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def download_release(package, id):
if ip is not None and not is_user_bot():
is_minetest = (request.headers.get("User-Agent") or "").startswith("Minetest")
reason = request.args.get("reason")
PackageDailyStats.update(package, is_minetest, reason)
PackageDailyStats.notify_download(package, is_minetest, reason)

key = make_download_key(ip, release.package)
if not has_key(key):
Expand Down
2 changes: 2 additions & 0 deletions app/flatpages/help/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ curl -X DELETE https://content.minetest.net/api/delete-token/ \
* `reason_new`: list of integers per day.
* `reason_dependency`: list of integers per day.
* `reason_update`: list of integers per day.
* `views_minetest`: list of integers per day.
* GET `/api/package_stats/`
* Returns last 30 days of daily stats for _all_ packages.
* An object with the following keys:
Expand Down Expand Up @@ -437,6 +438,7 @@ Example:
* `reason_new`: list of integers per day.
* `reason_dependency`: list of integers per day.
* `reason_update`: list of integers per day.
* `views_minetest`: list of integers per day.


## Topics
Expand Down
5 changes: 3 additions & 2 deletions app/logic/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def daterange(start_date, end_date):


keys = ["platform_minetest", "platform_other", "reason_new",
"reason_dependency", "reason_update"]
"reason_dependency", "reason_update", "views_minetest"]


def flatten_data(stats):
Expand Down Expand Up @@ -78,7 +78,8 @@ def get_package_stats_for_user(user: User, start_date: Optional[datetime.date],
func.sum(PackageDailyStats.platform_other).label("platform_other"),
func.sum(PackageDailyStats.reason_new).label("reason_new"),
func.sum(PackageDailyStats.reason_dependency).label("reason_dependency"),
func.sum(PackageDailyStats.reason_update).label("reason_update")) \
func.sum(PackageDailyStats.reason_update).label("reason_update"),
func.sum(PackageDailyStats.views_minetest).label("views_minetest")) \
.filter(PackageDailyStats.package.has(author_id=user.id))

if start_date:
Expand Down
20 changes: 19 additions & 1 deletion app/models/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -1437,8 +1437,10 @@ class PackageDailyStats(db.Model):
reason_dependency = db.Column(db.Integer, nullable=False, default=0)
reason_update = db.Column(db.Integer, nullable=False, default=0)

views_minetest = db.Column(db.Integer, nullable=False, default=0)

@staticmethod
def update(package: Package, is_minetest: bool, reason: str):
def notify_download(package: Package, is_minetest: bool, reason: str):
date = datetime.datetime.utcnow().date()

to_update = dict()
Expand Down Expand Up @@ -1470,3 +1472,19 @@ def update(package: Package, is_minetest: bool, reason: str):

conn = db.session.connection()
conn.execute(stmt)

@staticmethod
def notify_view(package: Package):
date = datetime.datetime.utcnow().date()

to_update = {"views_minetest": PackageDailyStats.views_minetest + 1}
kwargs = {"package_id": package.id, "date": date, "views_minetest": 1}

stmt = insert(PackageDailyStats).values(**kwargs)
stmt = stmt.on_conflict_do_update(
index_elements=[PackageDailyStats.package_id, PackageDailyStats.date],
set_=to_update
)

conn = db.session.connection()
conn.execute(stmt)
10 changes: 10 additions & 0 deletions app/public/static/js/package_charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ async function load_data() {
};
new Chart(ctx, config);
}

{
const ctx = document.getElementById("chart-views").getContext("2d");
const data = {
datasets: [
{ label: "Luanti", data: getData(json.views_minetest) },
],
};
setup_chart(ctx, data, annotations);
}
}


Expand Down
7 changes: 6 additions & 1 deletion app/rediscache.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.

from . import redis_client
from .models import Package

# This file acts as a facade between the rest of the code and redis,
# and also means that the rest of the code avoids knowing about `app`
Expand All @@ -23,10 +24,14 @@
EXPIRY_TIME_S = 2*7*24*60*60 # 2 weeks


def make_download_key(ip, package):
def make_download_key(ip: str, package: Package):
return "{}/{}/{}".format(ip, package.author.username, package.name)


def make_view_key(ip: str, package: Package):
return "view/{}/{}/{}".format(ip, package.author.username, package.name)


def set_temp_key(key, v):
redis_client.set(key, v, ex=EXPIRY_TIME_S)

Expand Down
8 changes: 7 additions & 1 deletion app/templates/macros/stats.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<script src="/static/libs/chart.min.js"></script>
<script src="/static/libs/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/static/libs/chartjs-plugin-annotation.min.js"></script>
<script src="/static/js/package_charts.js?v=2"></script>
<script src="/static/js/package_charts.js?v=3"></script>
{% endmacro %}


Expand Down Expand Up @@ -118,6 +118,12 @@ <h3 class="mt-5">{{ _("Downloads by Reason") }}</h3>
</div>
</div>

<h3 class="mt-5">{{ _("Views inside Luanti") }}</h3>
<p>
{{ _("Number of package page views inside the Luanti client. v5.10 and later only.") }}
</p>
<canvas id="chart-views" class="chart"></canvas>

<h3 style="margin-top: 6em;">{{ _("Need more stats?") }}</h3>
<p>
{{ _("Check out the ContentDB Grafana dashboard for CDB-wide stats") }}
Expand Down
26 changes: 26 additions & 0 deletions migrations/versions/d52f6901b707_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""empty message
Revision ID: d52f6901b707
Revises: daa040b727b2
Create Date: 2024-10-22 21:18:23.929298
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = 'd52f6901b707'
down_revision = 'daa040b727b2'
branch_labels = None
depends_on = None


def upgrade():
with op.batch_alter_table('package_daily_stats', schema=None) as batch_op:
batch_op.add_column(sa.Column('views_minetest', sa.Integer(), nullable=False, server_default="0"))


def downgrade():
with op.batch_alter_table('package_daily_stats', schema=None) as batch_op:
batch_op.drop_column('views_minetest')

0 comments on commit 8ecc856

Please sign in to comment.