-
Notifications
You must be signed in to change notification settings - Fork 359
[ENG-11735] | record download events #11823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
sh-andriy
merged 4 commits into
feature/download-telemetry
from
feature/ENG-11735-record-download-events
Jul 23, 2026
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
d941b58
feat(osf): ENG-11735 record download events for files and zips
sh-andriy e0c8dd0
feat(osf): ENG-11735 record download events for files and zips
sh-andriy 91493e5
feat(osf): ENG-11735 record download events for files and zips
sh-andriy 65c87bb
feat(osf): ENG-11735 record download events for files and zips
sh-andriy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| import functools | ||
| import logging | ||
|
|
||
| from addons.osfstorage.settings import DEFAULT_REGION_NAME | ||
| from framework.celery_tasks import app | ||
| from framework.postcommit_tasks.handlers import enqueue_postcommit_task | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| # Set on every user until they pick something in their profile, so it says nothing | ||
| # about where they actually are. | ||
| UNSET_USER_TIMEZONE = 'Etc/UTC' | ||
|
|
||
|
|
||
| def never_breaks_downloads(fn): | ||
| """Swallow and log anything this raises. | ||
|
|
||
| Wraps the whole capture, not just the write — gathering the values is as capable of | ||
| raising as storing them is, and neither is a reason for a download to fail. | ||
| """ | ||
| @functools.wraps(fn) | ||
| def wrapped(*args, **kwargs): | ||
| try: | ||
| return fn(*args, **kwargs) | ||
| except Exception: | ||
| logger.exception('Failed to record a download event') | ||
| return wrapped | ||
|
|
||
|
|
||
| @never_breaks_downloads | ||
| def record_download(**kwargs): | ||
| """Enqueue a :class:`DownloadEvent` write.""" | ||
| enqueue_postcommit_task(write_download_event, (), kwargs, celery=True) | ||
|
|
||
|
|
||
| @app.task(max_retries=5, default_retry_delay=60) | ||
| def write_download_event( | ||
| download_type, | ||
| resource_guid='', | ||
| path='', | ||
| file_id=None, | ||
| version_identifier=None, | ||
| size_bytes=None, | ||
| storage_region_id=None, | ||
| zip_completed=None, | ||
| user_guid=None, | ||
| ip=None, | ||
| source_area='', | ||
| tz='', | ||
| ): | ||
| """Resolve the expensive bits and write one row. | ||
|
|
||
| Callers hand over identifiers rather than loaded objects so that the download request | ||
| itself does no extra queries — everything that needs a lookup is resolved here. | ||
| """ | ||
| from osf.models import BaseFileNode, DownloadEvent, OSFUser | ||
|
|
||
| user = OSFUser.load(user_guid) if user_guid else None | ||
| file_node = BaseFileNode.load(file_id) if file_id else None | ||
| file_version = _load_file_version(file_node, version_identifier) | ||
|
|
||
| if file_version is not None: | ||
| if size_bytes is None: | ||
| size_bytes = file_version.size | ||
| if storage_region_id is None: | ||
| storage_region_id = file_version.region_id | ||
|
|
||
| storage_region = _region_name(storage_region_id) or _resource_region_name(resource_guid) | ||
|
|
||
| if not path and file_node is not None: | ||
| path = getattr(file_node, 'materialized_path', '') or '' | ||
|
|
||
| DownloadEvent.objects.create( | ||
| download_type=download_type, | ||
| resource_guid=_truncate(resource_guid, 255), | ||
| path=path or '', | ||
| size_bytes=size_bytes if size_bytes is not None and size_bytes >= 0 else None, | ||
| zip_completed=zip_completed, | ||
| storage_region=_truncate(storage_region, 64), | ||
| user_region=_truncate(derive_user_region(tz, user, storage_region), 64), | ||
| ip=ip or None, | ||
| source_area=_truncate(source_area, 128), | ||
| user=user, | ||
| ) | ||
|
|
||
|
|
||
| def derive_user_region(tz, user, storage_region): | ||
| """Best available guess at where the user is, most to least trustworthy. | ||
|
|
||
| The live browser timezone is the only real signal; the rest are fallbacks so the | ||
| dashboard isn't mostly blank. An empty string means we genuinely don't know, which | ||
| is more useful than a wrong guess. | ||
| """ | ||
| if tz: | ||
| return tz | ||
|
|
||
| profile_timezone = getattr(user, 'timezone', '') | ||
| if profile_timezone and profile_timezone != UNSET_USER_TIMEZONE: | ||
| return profile_timezone | ||
|
|
||
| # Everything defaults to the US region, so it only tells us something when it's been | ||
| # deliberately changed. | ||
| if storage_region and storage_region != DEFAULT_REGION_NAME: | ||
| return storage_region | ||
|
|
||
| return '' | ||
|
|
||
|
|
||
| def _load_file_version(file_node, version_identifier): | ||
| """The version that was served, for its size and region.""" | ||
| if file_node is None: | ||
| return None | ||
|
|
||
| from osf.models import FileVersion | ||
|
|
||
| versions = FileVersion.objects.filter(basefilenode=file_node) | ||
| if version_identifier: | ||
| return versions.filter(identifier=version_identifier).first() | ||
| return versions.order_by('-created').first() | ||
|
|
||
|
|
||
| def _region_name(region_id): | ||
| if not region_id: | ||
| return '' | ||
|
|
||
| from addons.osfstorage.models import Region | ||
|
|
||
| region = Region.objects.filter(id=region_id).first() | ||
| return region.name if region else '' | ||
|
|
||
|
|
||
| def _resource_region_name(resource_guid): | ||
| """Where a zip was served from — zips have no single file version to read it off.""" | ||
| if not resource_guid: | ||
| return '' | ||
|
|
||
| from osf.models import Guid | ||
|
|
||
| resource, _ = Guid.load_referent(resource_guid) | ||
| region = getattr(resource, 'osfstorage_region', None) | ||
| return getattr(region, 'name', '') or '' | ||
|
|
||
|
|
||
| def _truncate(value, max_length): | ||
| """Keep user-controllable values inside their column. | ||
|
|
||
| ``source`` and ``tz`` arrive off the query string, so they're whatever the caller | ||
| put there. | ||
| """ | ||
| return (value or '')[:max_length] | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe it's better to log what kind of issue happened during record downloading?