Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions addons/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@
DownloadEvent,
)
from osf.utils import permissions
from osf.utils.download_telemetry import never_breaks_downloads, record_download
from osf.utils.download_telemetry import (
classify_download_channel,
never_breaks_downloads,
record_download,
)
from osf.external.gravy_valet import request_helpers
from website.profile.utils import get_profile_image_url
from website.project import decorators
Expand Down Expand Up @@ -205,6 +209,11 @@ def _record_file_download(target, file_node, query_params, auth, version=None):
if _download_request_is_from_mfr(query_params):
return

source_area = query_params.get('source', '')
# a browser navigating a download link never sends an Authorization header; an
# API/OAuth client does, so a bearer token is a reliable "this is programmatic" signal
is_api_token = 'bearer' in request.headers.get('Authorization', '').lower()

record_download(
download_type=DownloadEvent.FILE,
resource_guid=getattr(target, '_id', '') or '',
Expand All @@ -214,7 +223,8 @@ def _record_file_download(target, file_node, query_params, auth, version=None):
user_guid=getattr(getattr(auth, 'user', None), '_id', None),
ip=request.remote_addr,
user_agent=request.headers.get('User-Agent', ''),
source_area=query_params.get('source', ''),
source_area=source_area,
download_channel=classify_download_channel(source_area, is_api_token=is_api_token),
tz=query_params.get('tz', ''),
)

Expand All @@ -237,6 +247,11 @@ def _record_zip_download(payload):
# The provider root is the whole project; anything below it is one folder.
is_whole_project = not materialized.strip('/')

source_area = action_meta.get('source', '')
# The WaterButler callback doesn't carry the original request's auth, so we can't tell
# an API zip from a crawler here — is_api_token stays False and those fold into OTHER.
# The source tag still tells frontend zips apart from the rest.

record_download(
download_type=DownloadEvent.PROJECT if is_whole_project else DownloadEvent.FOLDER_ZIP,
resource_guid=metadata.get('nid') or '',
Expand All @@ -248,7 +263,8 @@ def _record_zip_download(payload):
user_guid=(payload.get('auth') or {}).get('id'),
ip=action_meta.get('ip'),
user_agent=(payload.get('request_meta') or {}).get('user_agent', ''),
source_area=action_meta.get('source', ''),
source_area=source_area,
download_channel=classify_download_channel(source_area),
tz=action_meta.get('tz', ''),
)

Expand Down
16 changes: 16 additions & 0 deletions admin/templates/admin/input_filter.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{% load i18n %}
<h3>{% blocktranslate with filter_title=title %}By {{ filter_title }}{% endblocktranslate %}</h3>
<ul class="admin-filter-{{ spec.parameter_name }}">
<li>
<form method="get" action="">
{% for key, value in choices.0.query_parts %}
<input type="hidden" name="{{ key }}" value="{{ value }}">
{% endfor %}
<input type="text"
name="{{ spec.parameter_name }}"
value="{{ spec.value|default_if_none:'' }}"
placeholder="{{ spec.title }}"
style="width: 95%; margin: 4px 0;">
</form>
</li>
</ul>
118 changes: 107 additions & 11 deletions admin/templates/download_events/download_events.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,47 @@
th { font-size: 12px; color: #777; }
td { padding: 8px 10px; border-bottom: 1px solid #505050; color: #aaa; }
.empty-state { color: #777; font-size: 13px; }
.active-filters {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 0 0 18px;
}
.active-filters-label {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #777;
}
.filter-chip {
font-size: 12px;
color: #cfcfcf;
background: #3a3a3a;
border: 1px solid #505050;
border-radius: 12px;
padding: 3px 10px;
}
.filter-chip .chip-label { color: #8f8f8f; }
.unknown-note {
font-size: 12px;
color: #9c9c9c;
margin: 10px 0 0;
}
.unknown-note strong { color: #cfcfcf; font-weight: 600; }
</style>
<div class="download-events-dashboard">
<div class="dashboard-header">
<h2>Download telemetry dashboard</h2>
<p>Summaries and charts are scoped to the same filters and search terms as the table below.</p>
<div class="active-filters">
<span class="active-filters-label">Applied filters:</span>
{% for applied in download_events_active_filters %}
<span class="filter-chip"><span class="chip-label">{{ applied.label }}:</span> {{ applied.value }}</span>
{% empty %}
<span class="filter-chip">None — showing the full current range</span>
{% endfor %}
</div>
</div>

<div class="dashboard-grid">
Expand All @@ -85,15 +121,15 @@ <h2>Download telemetry dashboard</h2>
</div>
<div class="dashboard-card summary-card">
<div class="card-label">Total GB</div>
<div class="card-value">{{ download_events_dashboard.summary.total_gb|floatformat:2 }}</div>
<div class="card-value">{{ download_events_dashboard.summary.total_gb|floatformat:2 }}{{ download_events_dashboard.summary.total_gb_tb_suffix }}</div>
</div>
<div class="dashboard-card summary-card">
<div class="card-label">Unique users</div>
<div class="card-value">{{ download_events_dashboard.summary.unique_users }}</div>
</div>
<div class="dashboard-card summary-card">
<div class="card-label">File vs zip (including whole-project zip) (GB)</div>
<div class="card-value compact-value">{{ download_events_dashboard.split.file.gb|floatformat:2 }} / {{ download_events_dashboard.split.zip.gb|floatformat:2 }}</div>
<div class="card-value compact-value">{{ download_events_dashboard.split.file.gb|floatformat:2 }}{{ download_events_dashboard.split.file.gb_tb_suffix }} / {{ download_events_dashboard.split.zip.gb|floatformat:2 }}{{ download_events_dashboard.split.zip.gb_tb_suffix }}</div>
</div>
<div class="dashboard-card summary-card">
<div class="card-label">Failed zip downloads (server or provider error)</div>
Expand Down Expand Up @@ -127,6 +163,15 @@ <h2>Download telemetry dashboard</h2>
<div class="chart-container">
<canvas id="storageRegionChart"></canvas>
</div>
{% with unknown=download_events_dashboard.storage_regions_unknown %}
{% if unknown %}
<p class="unknown-note">
Not shown (region unknown): <strong>{{ unknown.downloads }}</strong> downloads ·
<strong>{{ unknown.gb|floatformat:2 }}</strong> GB
— Files: {{ unknown.file_count }} · Zips: {{ unknown.zip_count }}
</p>
{% endif %}
{% endwith %}
</div>

<div class="dashboard-card chart-card">
Expand All @@ -140,6 +185,15 @@ <h2>Download telemetry dashboard</h2>
<div class="chart-container">
<canvas id="userRegionChart"></canvas>
</div>
{% with unknown=download_events_dashboard.user_regions_unknown %}
{% if unknown %}
<p class="unknown-note">
Not shown (region unknown): <strong>{{ unknown.downloads }}</strong> downloads ·
<strong>{{ unknown.gb|floatformat:2 }}</strong> GB
— Files: {{ unknown.file_count }} · Zips: {{ unknown.zip_count }}
</p>
{% endif %}
{% endwith %}
</div>

<div class="dashboard-card chart-card">
Expand Down Expand Up @@ -224,6 +278,33 @@ <h4 style="margin:0 0 12px;color:#aaa;text-align:center;">
</tbody>
</table>
</div>
<div class="dashboard-card table-card">
<div class="card-label">By channel (frontend / API / other)</div>
<table>
<thead>
<tr>
<th>Channel</th>
<th>GB</th>
<th>Downloads</th>
</tr>
</thead>
<tbody>
{% for channel in download_events_dashboard.channels %}
<tr>
<td>{{ channel.name }}</td>
<td>{{ channel.gb|floatformat:2 }}</td>
<td>{{ channel.downloads }}</td>
</tr>
{% empty %}
<tr><td colspan="3">No activity in the current range.</td></tr>
{% endfor %}
</tbody>
</table>
<p class="unknown-note">
Zips can only be split frontend vs. other — API zip downloads aren't
distinguishable at capture, so they fall under "Other / direct".
</p>
</div>
<div class="dashboard-card table-card">
<div class="card-label">By storage provider</div>
<table>
Expand All @@ -246,6 +327,14 @@ <h4 style="margin:0 0 12px;color:#aaa;text-align:center;">
{% endfor %}
</tbody>
</table>
{% with unknown=download_events_dashboard.storage_providers_unknown %}
{% if unknown %}
<p class="unknown-note">
No provider recorded: <strong>{{ unknown.downloads }}</strong> downloads ·
<strong>{{ unknown.gb|floatformat:2 }}</strong> GB
</p>
{% endif %}
{% endwith %}
</div>
</div>
</div>
Expand Down Expand Up @@ -561,6 +650,20 @@ <h4 style="margin:0 0 12px;color:#aaa;text-align:center;">
}
);

// A zero slice ("File" or "Zip") has no arc, so don't pop a "File 0" / "Zip 0"
// tooltip for it — that reads as a bug when a filter zeroes one side out.
const doughnutOptions = {
...commonOptions,
cutout: "60%",
plugins: {
...commonOptions.plugins,
tooltip: {
...commonOptions.plugins.tooltip,
filter: (item) => item.parsed !== 0
}
}
};

new Chart(
document.getElementById("downloadsSplitChart"),
{
Expand All @@ -578,10 +681,7 @@ <h4 style="margin:0 0 12px;color:#aaa;text-align:center;">
]
}]
},
options: {
...commonOptions,
cutout: "60%"
}
options: doughnutOptions
}
);

Expand All @@ -602,11 +702,7 @@ <h4 style="margin:0 0 12px;color:#aaa;text-align:center;">
]
}]
},
options: {
...commonOptions,
cutout: "60%"
}

options: doughnutOptions
}
);
</script>
Expand Down
Loading