feat(shorebird_code_push_protocol): add activity heatmap response & cell model (#3836)
This commit is contained in:
@@ -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/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/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_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_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_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';
|
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_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/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/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.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/app_collaborator_role.dart';
|
export 'package:shorebird_code_push_protocol/src/models/app_collaborator_role.dart';
|
||||||
export 'package:shorebird_code_push_protocol/src/models/app_metadata.dart';
|
export 'package:shorebird_code_push_protocol/src/models/app_metadata.dart';
|
||||||
|
|||||||
+100
@@ -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<String, dynamic>` to a [GetActivityHeatmapResponse].
|
||||||
|
factory GetActivityHeatmapResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return parseFromJson(
|
||||||
|
'GetActivityHeatmapResponse',
|
||||||
|
json,
|
||||||
|
() => GetActivityHeatmapResponse(
|
||||||
|
cells: (json['cells'] as List)
|
||||||
|
.map<ActivityHeatmapCell>(
|
||||||
|
(e) => ActivityHeatmapCell.fromJson(e as Map<String, dynamic>),
|
||||||
|
)
|
||||||
|
.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<String, dynamic>? 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<ActivityHeatmapCell> 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<String, dynamic>`.
|
||||||
|
Map<String, dynamic> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, dynamic>` to an [ActivityHeatmapCell].
|
||||||
|
factory ActivityHeatmapCell.fromJson(Map<String, dynamic> 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<String, dynamic>? 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<String, dynamic>`.
|
||||||
|
Map<String, dynamic> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -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>[
|
||||||
|
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(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
+29
@@ -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(<String, dynamic>{}),
|
||||||
|
throwsFormatException,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user