From 4f43c064561116b92f2ccac4f164088ef9288940 Mon Sep 17 00:00:00 2001 From: Mac Date: Thu, 9 Jul 2026 11:22:55 -0600 Subject: [PATCH] feat(shorebird_code_push_protocol): add activity heatmap response & cell model (#3836) --- .../lib/shorebird_code_push_protocol.dart | 2 + .../get_activity_heatmap_response.dart | 100 ++++++++++++++++++ .../lib/src/models/activity_heatmap_cell.dart | 76 +++++++++++++ .../get_activity_heatmap_response_test.dart | 39 +++++++ .../models/activity_heatmap_cell_test.dart | 29 +++++ 5 files changed, 246 insertions(+) create mode 100644 packages/shorebird_code_push_protocol/lib/src/messages/get_activity_heatmap/get_activity_heatmap_response.dart create mode 100644 packages/shorebird_code_push_protocol/lib/src/models/activity_heatmap_cell.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/messages/get_activity_heatmap/get_activity_heatmap_response_test.dart create mode 100644 packages/shorebird_code_push_protocol/test/generated/models/activity_heatmap_cell_test.dart diff --git a/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart b/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart index de10e753..617e081e 100644 --- a/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart +++ b/packages/shorebird_code_push_protocol/lib/shorebird_code_push_protocol.dart @@ -24,6 +24,7 @@ export 'package:shorebird_code_push_protocol/src/messages/create_release_artifac export 'package:shorebird_code_push_protocol/src/messages/create_user/create_user_request.dart'; export 'package:shorebird_code_push_protocol/src/messages/error_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_active_hours/get_active_hours_response.dart'; +export 'package:shorebird_code_push_protocol/src/messages/get_activity_heatmap/get_activity_heatmap_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_apps/get_apps_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_gcp_download_speed_test_url/get_gcp_download_speed_test_url200_response.dart'; export 'package:shorebird_code_push_protocol/src/messages/get_gcp_upload_speed_test_url/get_gcp_upload_speed_test_url200_response.dart'; @@ -45,6 +46,7 @@ export 'package:shorebird_code_push_protocol/src/messages/update_app_collaborato export 'package:shorebird_code_push_protocol/src/messages/update_patch/update_patch_request.dart'; export 'package:shorebird_code_push_protocol/src/messages/update_release/update_release_request.dart'; export 'package:shorebird_code_push_protocol/src/models/active_hour_entry.dart'; +export 'package:shorebird_code_push_protocol/src/models/activity_heatmap_cell.dart'; export 'package:shorebird_code_push_protocol/src/models/app.dart'; export 'package:shorebird_code_push_protocol/src/models/app_collaborator_role.dart'; export 'package:shorebird_code_push_protocol/src/models/app_metadata.dart'; diff --git a/packages/shorebird_code_push_protocol/lib/src/messages/get_activity_heatmap/get_activity_heatmap_response.dart b/packages/shorebird_code_push_protocol/lib/src/messages/get_activity_heatmap/get_activity_heatmap_response.dart new file mode 100644 index 00000000..cf4a2dd3 --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/messages/get_activity_heatmap/get_activity_heatmap_response.dart @@ -0,0 +1,100 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; +import 'package:shorebird_code_push_protocol/src/models/activity_heatmap_cell.dart'; + +/// {@template get_activity_heatmap_response} +/// The response body for GET /apps/{appId}/metrics/activity-heatmap. A 7×24 +/// grid of average active devices per UTC weekday-hour, powering the +/// insights activity heatmap. +/// {@endtemplate} +@immutable +class GetActivityHeatmapResponse { + /// {@macro get_activity_heatmap_response} + const GetActivityHeatmapResponse({ + required this.cells, + required this.busiestDayOfWeekUtc, + required this.busiestHourUtc, + required this.lookbackDays, + required this.asOf, + }); + + /// Converts a `Map` to a [GetActivityHeatmapResponse]. + factory GetActivityHeatmapResponse.fromJson(Map json) { + return parseFromJson( + 'GetActivityHeatmapResponse', + json, + () => GetActivityHeatmapResponse( + cells: (json['cells'] as List) + .map( + (e) => ActivityHeatmapCell.fromJson(e as Map), + ) + .toList(), + busiestDayOfWeekUtc: + checkedKey(json, 'busiest_day_of_week_utc') as int?, + busiestHourUtc: checkedKey(json, 'busiest_hour_utc') as int?, + lookbackDays: json['lookback_days'] as int, + asOf: DateTime.parse(json['as_of'] as String), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static GetActivityHeatmapResponse? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return GetActivityHeatmapResponse.fromJson(json); + } + + /// 168 entries (7 weekdays × 24 hours), zero-filled, ordered by + /// day_of_week_utc (1–7) then hour_utc (0–23) ascending. + final List cells; + + /// UTC day-of-week (1–7) of the cell with the highest average active + /// devices, for peak-relative coloring and labeling. Null when there is + /// no data. + final int? busiestDayOfWeekUtc; + + /// UTC hour (0–23) of the busiest cell. Null when there is no data. + final int? busiestHourUtc; + + /// Number of days of history the heatmap is computed over. + final int lookbackDays; + + /// Server's UTC timestamp at the moment the response was constructed. + /// Not a freshness indicator for the underlying data, which is + /// refreshed by an hourly scheduled query and may lag by up to ~1 hour. + final DateTime asOf; + + /// Converts a [GetActivityHeatmapResponse] to a `Map`. + Map toJson() { + return { + 'cells': cells.map((e) => e.toJson()).toList(), + 'busiest_day_of_week_utc': busiestDayOfWeekUtc, + 'busiest_hour_utc': busiestHourUtc, + 'lookback_days': lookbackDays, + 'as_of': asOf.toIso8601String(), + }; + } + + @override + int get hashCode => Object.hashAll([ + listHash(cells), + busiestDayOfWeekUtc, + busiestHourUtc, + lookbackDays, + asOf, + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is GetActivityHeatmapResponse && + listsEqual(cells, other.cells) && + busiestDayOfWeekUtc == other.busiestDayOfWeekUtc && + busiestHourUtc == other.busiestHourUtc && + lookbackDays == other.lookbackDays && + asOf == other.asOf; + } +} diff --git a/packages/shorebird_code_push_protocol/lib/src/models/activity_heatmap_cell.dart b/packages/shorebird_code_push_protocol/lib/src/models/activity_heatmap_cell.dart new file mode 100644 index 00000000..ffe82d45 --- /dev/null +++ b/packages/shorebird_code_push_protocol/lib/src/models/activity_heatmap_cell.dart @@ -0,0 +1,76 @@ +import 'package:meta/meta.dart'; +import 'package:shorebird_code_push_protocol/model_helpers.dart'; + +/// {@template activity_heatmap_cell} +/// Average number of distinct active devices during one (UTC day-of-week, +/// UTC hour-of-day) cell, averaged across every occurrence of that weekday +/// in the lookback window (occurrences with no activity count as zero). +/// {@endtemplate} +@immutable +class ActivityHeatmapCell { + /// {@macro activity_heatmap_cell} + const ActivityHeatmapCell({ + required this.dayOfWeekUtc, + required this.hourUtc, + required this.averageActiveDevices, + }); + + /// Converts a `Map` to an [ActivityHeatmapCell]. + factory ActivityHeatmapCell.fromJson(Map json) { + return parseFromJson( + 'ActivityHeatmapCell', + json, + () => ActivityHeatmapCell( + dayOfWeekUtc: json['day_of_week_utc'] as int, + hourUtc: json['hour_utc'] as int, + averageActiveDevices: (json['average_active_devices'] as num) + .toDouble(), + ), + ); + } + + /// Convenience to create a nullable type from a nullable json object. + /// Useful when parsing optional fields. + static ActivityHeatmapCell? maybeFromJson(Map? json) { + if (json == null) { + return null; + } + return ActivityHeatmapCell.fromJson(json); + } + + /// Day of week in UTC, 1–7 where 1 = Sunday and 7 = Saturday. + final int dayOfWeekUtc; + + /// Hour of day in UTC, 0–23. + final int hourUtc; + + /// Mean distinct active devices seen during this UTC weekday-hour, + /// averaged over the weekday's occurrences in the window with implicit + /// zeros included. + final double averageActiveDevices; + + /// Converts an [ActivityHeatmapCell] to a `Map`. + Map toJson() { + return { + 'day_of_week_utc': dayOfWeekUtc, + 'hour_utc': hourUtc, + 'average_active_devices': averageActiveDevices, + }; + } + + @override + int get hashCode => Object.hashAll([ + dayOfWeekUtc, + hourUtc, + averageActiveDevices, + ]); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is ActivityHeatmapCell && + dayOfWeekUtc == other.dayOfWeekUtc && + hourUtc == other.hourUtc && + averageActiveDevices == other.averageActiveDevices; + } +} diff --git a/packages/shorebird_code_push_protocol/test/generated/messages/get_activity_heatmap/get_activity_heatmap_response_test.dart b/packages/shorebird_code_push_protocol/test/generated/messages/get_activity_heatmap/get_activity_heatmap_response_test.dart new file mode 100644 index 00000000..a766b973 --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/messages/get_activity_heatmap/get_activity_heatmap_response_test.dart @@ -0,0 +1,39 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('GetActivityHeatmapResponse', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = GetActivityHeatmapResponse( + cells: [ + ActivityHeatmapCell( + dayOfWeekUtc: 0, + hourUtc: 0, + averageActiveDevices: 0, + ), + ], + busiestDayOfWeekUtc: 0, + busiestHourUtc: 0, + lookbackDays: 0, + asOf: DateTime.utc(2024), + ); + final parsed = GetActivityHeatmapResponse.maybeFromJson( + instance.toJson(), + ); + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(GetActivityHeatmapResponse.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => GetActivityHeatmapResponse.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +} diff --git a/packages/shorebird_code_push_protocol/test/generated/models/activity_heatmap_cell_test.dart b/packages/shorebird_code_push_protocol/test/generated/models/activity_heatmap_cell_test.dart new file mode 100644 index 00000000..05c447d3 --- /dev/null +++ b/packages/shorebird_code_push_protocol/test/generated/models/activity_heatmap_cell_test.dart @@ -0,0 +1,29 @@ +// GENERATED — do not hand-edit. +import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart'; +import 'package:test/test.dart'; + +void main() { + group('ActivityHeatmapCell', () { + test('round-trips via maybeFromJson/toJson', () { + final instance = ActivityHeatmapCell( + dayOfWeekUtc: 0, + hourUtc: 0, + averageActiveDevices: 0, + ); + final parsed = ActivityHeatmapCell.maybeFromJson(instance.toJson()); + expect(parsed, equals(instance)); + expect(parsed.hashCode, equals(instance.hashCode)); + }); + + test('maybeFromJson returns null on null input', () { + expect(ActivityHeatmapCell.maybeFromJson(null), isNull); + }); + + test('maybeFromJson throws FormatException on invalid input', () { + expect( + () => ActivityHeatmapCell.maybeFromJson({}), + throwsFormatException, + ); + }); + }); +}