From 68decadb2ac071e9f4de809d8789da6dc9886ef3 Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Mon, 28 Oct 2024 10:04:42 +0000 Subject: [PATCH 1/3] Add widget for weekly downloads chart with downloads data --- app/lib/frontend/handlers/package.dart | 5 + .../templates/views/pkg/info_box.dart | 21 + app/lib/package/models.dart | 3 + .../service/download_counts/computations.dart | 14 + .../download_counts/download_counts.dart | 15 + .../download_counts/download_counts.g.dart | 16 + app/lib/shared/redis_cache.dart | 10 + .../pkg_score_page_with_downloads_chart.html | 495 ++++++++++++++++++ app/test/frontend/templates_test.dart | 28 + 9 files changed, 607 insertions(+) create mode 100644 app/test/frontend/golden/pkg_score_page_with_downloads_chart.html diff --git a/app/lib/frontend/handlers/package.dart b/app/lib/frontend/handlers/package.dart index 368d166a98..cdc90a957c 100644 --- a/app/lib/frontend/handlers/package.dart +++ b/app/lib/frontend/handlers/package.dart @@ -10,6 +10,7 @@ import 'package:_pub_shared/data/advisories_api.dart' import 'package:_pub_shared/utils/sdk_version_cache.dart'; import 'package:meta/meta.dart'; import 'package:neat_cache/neat_cache.dart'; +import 'package:pub_dev/service/download_counts/computations.dart'; import 'package:pub_dev/service/security_advisories/backend.dart'; import 'package:pub_dev/shared/versions.dart'; import 'package:pub_dev/task/backend.dart'; @@ -453,6 +454,8 @@ Future loadPackagePageData( final scoreCardFuture = scoreCardBackend .getScoreCardData(packageName, versionName, package: package); + final weeklyDownloadCountsFuture = getWeeklyDownloads(package.name!); + await Future.wait([ latestReleasesFuture, isLikedFuture, @@ -461,6 +464,7 @@ Future loadPackagePageData( assetFuture, isAdminFuture, scoreCardFuture, + weeklyDownloadCountsFuture, ]); final selectedVersion = await selectedVersionFuture; @@ -484,6 +488,7 @@ Future loadPackagePageData( scoreCard: await scoreCardFuture, isAdmin: await isAdminFuture, isLiked: await isLikedFuture, + weeklyDownloadCounts: await weeklyDownloadCountsFuture, ); } diff --git a/app/lib/frontend/templates/views/pkg/info_box.dart b/app/lib/frontend/templates/views/pkg/info_box.dart index c6a651a0c5..1612deb950 100644 --- a/app/lib/frontend/templates/views/pkg/info_box.dart +++ b/app/lib/frontend/templates/views/pkg/info_box.dart @@ -2,7 +2,9 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'package:_pub_shared/format/encoding.dart'; import 'package:pana/pana.dart'; +import 'package:pub_dev/service/download_counts/download_counts.dart'; import 'package:pubspec_parse/pubspec_parse.dart' as pubspek; import '../../../../package/models.dart'; @@ -69,6 +71,8 @@ d.Node packageInfoBoxNode({ } return d.fragment([ labeledScores, + if (data.weeklyDownloadCounts != null) + _downloadsChart(data.weeklyDownloadCounts!), if (thumbnailUrl != null) d.div(classes: [ 'detail-screenshot-thumbnail' @@ -105,6 +109,23 @@ d.Node packageInfoBoxNode({ ]); } +d.Node _downloadsChart(WeeklyDownloadCounts wdc) { + final container = d.div( + classes: ['weekly-downloads-sparkline'], + id: '-weekly-downloads-sparkline', + attributes: { + 'data-widget': 'weekly-sparkline', + 'data-sparkline-points': + _encodeForWeeklySparkline(wdc.weeklyDownloads, wdc.newestDate), + }); + return container; +} + +String _encodeForWeeklySparkline(List downloads, DateTime newestDate) { + final date = newestDate.toUtc().millisecondsSinceEpoch ~/ 1000; + return encodeIntsAsLittleEndianBase64String([date, ...downloads]); +} + d.Node _publisher(String? publisherId) { return _block( 'Publisher', diff --git a/app/lib/package/models.dart b/app/lib/package/models.dart index dae6f5dbb8..47d65c2c63 100644 --- a/app/lib/package/models.dart +++ b/app/lib/package/models.dart @@ -10,6 +10,7 @@ import 'package:clock/clock.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:pana/models.dart'; import 'package:pub_dev/service/download_counts/backend.dart'; +import 'package:pub_dev/service/download_counts/download_counts.dart'; import 'package:pub_dev/shared/markdown.dart'; import 'package:pub_semver/pub_semver.dart'; @@ -1132,6 +1133,7 @@ class PackagePageData { final ScoreCardData scoreCard; final bool isAdmin; final bool isLiked; + final WeeklyDownloadCounts? weeklyDownloadCounts; PackageView? _view; PackagePageData({ @@ -1143,6 +1145,7 @@ class PackagePageData { required this.scoreCard, required this.isAdmin, required this.isLiked, + required this.weeklyDownloadCounts, }) : latestReleases = latestReleases ?? package.latestReleases; bool get hasReadme => versionInfo.assets.contains(AssetKind.readme); diff --git a/app/lib/service/download_counts/computations.dart b/app/lib/service/download_counts/computations.dart index 7a428b52fb..c19527bfc0 100644 --- a/app/lib/service/download_counts/computations.dart +++ b/app/lib/service/download_counts/computations.dart @@ -6,11 +6,14 @@ import 'dart:math'; import 'package:gcloud/storage.dart'; import 'package:pub_dev/service/download_counts/backend.dart'; +import 'package:pub_dev/service/download_counts/download_counts.dart'; import 'package:pub_dev/service/download_counts/models.dart'; import 'package:pub_dev/shared/configuration.dart'; import 'package:pub_dev/shared/storage.dart'; import 'package:pub_dev/shared/utils.dart'; +import '../../shared/redis_cache.dart' show cache; + Future compute30DaysTotalTask() async { final allDownloadCounts = await downloadCountsBackend.listAllDownloadCounts(); final totals = await compute30DayTotals(allDownloadCounts); @@ -43,6 +46,17 @@ Future upload30DaysTotal(Map counts) async { jsonUtf8Encoder.convert(counts)); } +Future getWeeklyDownloads(String package) async { + return (await cache.weeklyDownloadCounts(package).get(() async { + final wdc = await computeWeeklyDownloads(package); + if (wdc.newestDate == null) { + return null; + } + return WeeklyDownloadCounts( + weeklyDownloads: wdc.weeklyDownloads, newestDate: wdc.newestDate!); + })); +} + /// Computes `weeklyDownloads` starting from `newestDate` for [package]. /// /// Each number in `weeklyDownloads` is the total number of downloads for diff --git a/app/lib/service/download_counts/download_counts.dart b/app/lib/service/download_counts/download_counts.dart index cdae2d8cd6..d37cf915d1 100644 --- a/app/lib/service/download_counts/download_counts.dart +++ b/app/lib/service/download_counts/download_counts.dart @@ -215,3 +215,18 @@ class CountData { _$CountDataFromJson(json); Map toJson() => _$CountDataToJson(this); } + +@JsonSerializable(includeIfNull: false) +class WeeklyDownloadCounts { + final List weeklyDownloads; + final DateTime newestDate; + + WeeklyDownloadCounts({ + required this.weeklyDownloads, + required this.newestDate, + }); + + factory WeeklyDownloadCounts.fromJson(Map json) => + _$WeeklyDownloadCountsFromJson(json); + Map toJson() => _$WeeklyDownloadCountsToJson(this); +} diff --git a/app/lib/service/download_counts/download_counts.g.dart b/app/lib/service/download_counts/download_counts.g.dart index 636b1c52d1..55275441b4 100644 --- a/app/lib/service/download_counts/download_counts.g.dart +++ b/app/lib/service/download_counts/download_counts.g.dart @@ -84,3 +84,19 @@ $Rec _$recordConvert<$Rec>( $Rec Function(Map) convert, ) => convert(value as Map); + +WeeklyDownloadCounts _$WeeklyDownloadCountsFromJson( + Map json) => + WeeklyDownloadCounts( + weeklyDownloads: (json['weeklyDownloads'] as List) + .map((e) => (e as num).toInt()) + .toList(), + newestDate: DateTime.parse(json['newestDate'] as String), + ); + +Map _$WeeklyDownloadCountsToJson( + WeeklyDownloadCounts instance) => + { + 'weeklyDownloads': instance.weeklyDownloads, + 'newestDate': instance.newestDate.toIso8601String(), + }; diff --git a/app/lib/shared/redis_cache.dart b/app/lib/shared/redis_cache.dart index 58b7672e0e..44745e586d 100644 --- a/app/lib/shared/redis_cache.dart +++ b/app/lib/shared/redis_cache.dart @@ -247,6 +247,16 @@ class CachePatterns { decode: (d) => CountData.fromJson(d as Map), ))[package]; + Entry weeklyDownloadCounts(String package) => _cache + .withPrefix('weekly-download-counts/') + .withTTL(Duration(hours: 6)) + .withCodec(utf8) + .withCodec(json) + .withCodec(wrapAsCodec( + encode: (WeeklyDownloadCounts wdc) => wdc.toJson(), + decode: (d) => WeeklyDownloadCounts.fromJson(d as Map), + ))[package]; + Entry> userPackageLikes(String userId) => _cache .withPrefix('user-package-likes/') .withTTL(Duration(minutes: 60)) diff --git a/app/test/frontend/golden/pkg_score_page_with_downloads_chart.html b/app/test/frontend/golden/pkg_score_page_with_downloads_chart.html new file mode 100644 index 0000000000..7a2005780a --- /dev/null +++ b/app/test/frontend/golden/pkg_score_page_with_downloads_chart.html @@ -0,0 +1,495 @@ + + + + + + + + + + + + + + + + + + + oxygen score | Dart package + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+

+ oxygen 1.2.0 + + copy "oxygen: ^1.2.0" to clipboard +
+ oxygen: ^1.2.0 + copied to clipboard +
+
+

+ +
+
+
+ SDK + Dart + Flutter +
+
+ Platform + Android + iOS + Linux + macOS + web + Windows +
+
+ +
+
+
+
+
+
+
+ +

oxygen is awesome

+

+ +

+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ 0 + +
+
likes
+
+
+
+ 54 + / 70 +
+
pub points
+
+
+
+ 3 + % +
+
popularity
+
+
+

+ We analyzed this package + %%x-ago%% + , and awarded it 54 pub points (of a possible 70): +

+
+
+
+
+ failed +
+
Fake conventions
+
+ 18 + / + 30 + trigger folding of the section +
+
+
+
+
+

+ + 18/30 points: Package layout +

+
    +
  • Package layout score randomly set to 18...
  • +
+
+
+
+
+
+
+
+ OK +
+
Fake documentation
+
+ 36 + / + 40 + trigger folding of the section +
+
+
+
+
+

+ + 36/40 points: Example +

+
    +
  • Example score randomly set to 36...
  • +
+
+
+
+
+
+

+ Analyzed with Pana + %%pana-version%% + , Dart + %%stable-dart-version%% + . +

+

+ Check the + analysis log + for details. +

+
+
+
+
+ +
+ +
+ + +
+ + + + diff --git a/app/test/frontend/templates_test.dart b/app/test/frontend/templates_test.dart index 786bfa3462..a1c3badd69 100644 --- a/app/test/frontend/templates_test.dart +++ b/app/test/frontend/templates_test.dart @@ -36,6 +36,7 @@ import 'package:pub_dev/publisher/backend.dart'; import 'package:pub_dev/publisher/models.dart'; import 'package:pub_dev/scorecard/backend.dart'; import 'package:pub_dev/search/search_service.dart'; +import 'package:pub_dev/service/download_counts/backend.dart'; import 'package:pub_dev/service/youtube/backend.dart'; import 'package:pub_dev/shared/utils.dart' show shortDateFormat; import 'package:pub_dev/shared/versions.dart'; @@ -203,6 +204,33 @@ void main() { }); }); + testWithProfile( + 'package score page with downloads chart div with data', + processJobsWithFakeRunners: true, + fn: () async { + final date = DateTime.parse('2024-01-07'); + final versionsCounts = { + '1.2.0': 200, + '2.0.0-alpha': 2, + '2.0.0': 2, + '2.1.0': 2, + '3.1.0': 2, + '4.0.0-0': 2, + '6.1.0': 2, + }; + await downloadCountsBackend.updateDownloadCounts( + 'oxygen', versionsCounts, date); + final data = await loadPackagePageDataByName( + 'oxygen', '1.2.0', AssetKind.changelog); + final html = renderPkgScorePage(data); + expectGoldenFile(html, 'pkg_score_page_with_downloads_chart.html', + timestamps: { + 'published': data.package.created, + 'updated': data.version.created, + }); + }, + ); + testWithProfile( 'package show page - with version', processJobsWithFakeRunners: true, From 382cab773c97797114e8fe2cc30d6d969bfe47ad Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Wed, 30 Oct 2024 21:18:42 +0000 Subject: [PATCH 2/3] name --- app/lib/frontend/templates/views/pkg/info_box.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/frontend/templates/views/pkg/info_box.dart b/app/lib/frontend/templates/views/pkg/info_box.dart index 1612deb950..8054df8034 100644 --- a/app/lib/frontend/templates/views/pkg/info_box.dart +++ b/app/lib/frontend/templates/views/pkg/info_box.dart @@ -115,7 +115,7 @@ d.Node _downloadsChart(WeeklyDownloadCounts wdc) { id: '-weekly-downloads-sparkline', attributes: { 'data-widget': 'weekly-sparkline', - 'data-sparkline-points': + 'data-weekly-sparkline-points': _encodeForWeeklySparkline(wdc.weeklyDownloads, wdc.newestDate), }); return container; From cf1580b4ac79502c4c3ba163f4a379c08f214a2a Mon Sep 17 00:00:00 2001 From: Sarah Zakarias Date: Thu, 31 Oct 2024 08:42:18 +0000 Subject: [PATCH 3/3] update golden --- .../frontend/golden/pkg_score_page_with_downloads_chart.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/test/frontend/golden/pkg_score_page_with_downloads_chart.html b/app/test/frontend/golden/pkg_score_page_with_downloads_chart.html index 7a2005780a..d3129818cd 100644 --- a/app/test/frontend/golden/pkg_score_page_with_downloads_chart.html +++ b/app/test/frontend/golden/pkg_score_page_with_downloads_chart.html @@ -333,7 +333,7 @@

popularity
-
+

Publisher

unverified uploader @@ -411,7 +411,7 @@

Publisher

unverified uploader