Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3068762
feat(osf): ENG-11734 add DownloadEvent model and migration with dashb…
sh-andriy Jul 15, 2026
6f69855
feat(osf): ENG-11734 add DownloadEvent model and migration with dashb…
sh-andriy Jul 15, 2026
ef80d92
feat(osf): ENG-11734 add DownloadEvent model and migration with dashb…
sh-andriy Jul 16, 2026
ed3ea81
feat(osf): ENG-11734 add DownloadEvent model and migration with dashb…
sh-andriy Jul 16, 2026
9cda5cc
Merge pull request #11810 from CenterForOpenScience/ENG-11734-downloa…
sh-andriy Jul 16, 2026
b016b8d
show download events in django admin based on group
ihorsokhanexoft Jul 20, 2026
d85c41b
added default ordering, filters and search for download events
ihorsokhanexoft Jul 20, 2026
af3a157
added charts
ihorsokhanexoft Jul 21, 2026
fc05bbf
fixed top by volume ordering
ihorsokhanexoft Jul 21, 2026
b772087
improved charts with chart.js
ihorsokhanexoft Jul 21, 2026
5d0e9fe
fixed hover effect, make size_bytes 0 by default to avoid incorrect o…
ihorsokhanexoft Jul 21, 2026
c9734c3
clean up
ihorsokhanexoft Jul 21, 2026
4932caa
display project title (if present) and guid in top 10 projects by volume
ihorsokhanexoft Jul 22, 2026
d941b58
feat(osf): ENG-11735 record download events for files and zips
sh-andriy Jul 22, 2026
e0c8dd0
feat(osf): ENG-11735 record download events for files and zips
sh-andriy Jul 22, 2026
91493e5
feat(osf): ENG-11735 record download events for files and zips
sh-andriy Jul 23, 2026
65c87bb
feat(osf): ENG-11735 record download events for files and zips
sh-andriy Jul 23, 2026
0315383
Merge pull request #11823 from CenterForOpenScience/feature/ENG-11735…
sh-andriy Jul 23, 2026
005c9bb
Merge remote-tracking branch 'upstream/feature/download-telemetry' in…
sh-andriy Jul 23, 2026
b83c3d8
fix(osf): ENG-11739 gate the dashboard on group membership only
sh-andriy Jul 23, 2026
3704e4f
Merge pull request #11822 from ihorsokhanexoft/ENG-11739
sh-andriy Jul 23, 2026
8a2fb1f
tests for model, wb
bodintsov Jul 23, 2026
f5e92df
Merge pull request #11826 from bodintsov/feature/download-telemetry-t…
sh-andriy Jul 24, 2026
dc77796
Merge branch 'develop' into feature/download-telemetry
sh-andriy Jul 24, 2026
6d0dfc2
feat(osf): ENG-11826 record failed zip downloads and their status code
sh-andriy Jul 27, 2026
6214eb2
feat(osf): ENG-11826 surface failed zip downloads on the dashboard
sh-andriy Jul 27, 2026
787464d
Merge pull request #11830 from CenterForOpenScience/hotfix/ENG-11826-…
sh-andriy Jul 27, 2026
30c1d61
feat(osf): add storage provider to download telemetry (#11834)
sh-andriy Jul 28, 2026
2e1c28a
feat(osf): break down download requests by type on the region charts
sh-andriy Jul 31, 2026
c04769b
Merge pull request #11844 from CenterForOpenScience/feature/dashboard…
sh-andriy Jul 31, 2026
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
83 changes: 78 additions & 5 deletions addons/base/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,11 @@
DraftRegistration,
Guid,
FileVersionUserMetadata,
FileVersion, NotificationTypeEnum
FileVersion, NotificationTypeEnum,
DownloadEvent,
)
from osf.utils import permissions
from osf.utils.download_telemetry import 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 @@ -181,6 +183,74 @@ def _download_is_from_mfr(waterbutler_data):
)


def _download_request_is_from_mfr(query_params):
"""Same question as :func:`_download_is_from_mfr`, asked of a browser request.

Here the render mode is a query param on the request itself rather than something
WaterButler reported to us.
"""
return bool(
request.headers.get('X-Cos-Mfr-Render-Request', None) or
query_params.get('mode') == 'render'
)


@never_breaks_downloads
def _record_file_download(target, file_node, query_params, auth, version=None):
"""Record a single-file download from the redirect view.

Only identifiers are handed over — the size, region and materialized path are looked
up in the celery task so the download itself doesn't pay for them.
"""
if _download_request_is_from_mfr(query_params):
return

record_download(
download_type=DownloadEvent.FILE,
resource_guid=getattr(target, '_id', '') or '',
file_id=getattr(file_node, '_id', None),
version_identifier=getattr(version, 'identifier', None),
storage_provider=getattr(file_node, 'provider', '') or '',
user_guid=getattr(getattr(auth, 'user', None), '_id', None),
ip=request.remote_addr,
source_area=query_params.get('source', ''),
tz=query_params.get('tz', ''),
)


@never_breaks_downloads
def _record_zip_download(payload):
"""Record a folder or project zip from the WaterButler callback.

Zips are requested straight from WaterButler, so this callback is the only point at
which we hear about them. The user's IP and the ``source``/``tz`` link tags are
forwarded to us in ``action_meta``.
"""
metadata = payload.get('metadata') or {}
action_meta = payload.get('action_meta') or {}

if action_meta.get('is_mfr_render'):
return

materialized = metadata.get('materialized') or metadata.get('path') or ''
# The provider root is the whole project; anything below it is one folder.
is_whole_project = not materialized.strip('/')

record_download(
download_type=DownloadEvent.PROJECT if is_whole_project else DownloadEvent.FOLDER_ZIP,
resource_guid=metadata.get('nid') or '',
path=materialized,
storage_provider=metadata.get('provider') or '',
size_bytes=action_meta.get('bytes_downloaded'),
zip_completed=action_meta.get('completed'),
status_code=action_meta.get('status_code'),
user_guid=(payload.get('auth') or {}).get('id'),
ip=action_meta.get('ip'),
source_area=action_meta.get('source', ''),
tz=action_meta.get('tz', ''),
)


def make_auth(user):
if user is not None:
return {
Expand Down Expand Up @@ -483,11 +553,11 @@ def create_waterbutler_log(payload, **kwargs):
with transaction.atomic():
try:
auth = payload['auth']
# Don't log download actions
# Downloads produce no NodeLog, but zips are recorded for telemetry here —
# they never pass through the redirect view where single files are caught.
if payload['action'] in DOWNLOAD_ACTIONS:
guid_id = payload['metadata'].get('nid')

node, _ = Guid.load_referent(guid_id)
if payload['action'] == 'download_zip':
_record_zip_download(payload)
return {'status': 'success'}

user = OSFUser.load(auth['id'])
Expand Down Expand Up @@ -983,6 +1053,7 @@ def addon_view_or_download_file(auth, path, provider, **kwargs):
}))

if action == 'download':
_record_file_download(target, file_node, extras, auth, version=version)
format = extras.get('format')
_, extension = os.path.splitext(file_node.name)
# avoid rendering files with the same format type.
Expand Down Expand Up @@ -1045,6 +1116,8 @@ def persistent_file_download(auth, **kwargs):

query_params = request.args.to_dict()

_record_file_download(file.target, file, query_params, auth)

return make_response(
'', http_status.HTTP_302_FOUND, {
'Location': file.generate_waterbutler_url(**query_params),
Expand Down
1 change: 1 addition & 0 deletions admin/base/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
'guardian',
'waffle',
'elasticsearch_metrics.apps.ElasticsearchMetricsConfig',
'rangefilter',

# OSF
'osf',
Expand Down
Loading