feat(shorebird_code_push_protocol): add active-hours response types (#3812)

This commit is contained in:
Mac
2026-06-10 10:57:20 -06:00
committed by GitHub
parent 70b89d40ec
commit 86842b538a
6 changed files with 239 additions and 0 deletions
+1
View File
@@ -74,6 +74,7 @@ words:
- LOCALAPPDATA
- logcat
- longpaths
- lookback # active-hours "best time to release" lookback window
- lproj
- madd # From ./packages/redis_client
- mbps
@@ -23,6 +23,7 @@ export 'package:shorebird_code_push_protocol/src/messages/create_release_artifac
export 'package:shorebird_code_push_protocol/src/messages/create_release_artifact/create_release_artifact_response.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/get_active_hours/get_active_hours_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';
@@ -41,6 +42,7 @@ export 'package:shorebird_code_push_protocol/src/messages/promote_patch/promote_
export 'package:shorebird_code_push_protocol/src/messages/update_app_collaborator/update_app_collaborator_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/models/active_hour_entry.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';
@@ -0,0 +1,110 @@
import 'package:meta/meta.dart';
import 'package:shorebird_code_push_protocol/model_helpers.dart';
import 'package:shorebird_code_push_protocol/src/models/active_hour_entry.dart';
/// {@template get_active_hours_response}
/// The response body for GET /apps/{appId}/metrics/active-hours. Powers the
/// "best time to release" recommendation: the consecutive low-activity UTC
/// window when the app's users are least active.
/// {@endtemplate}
@immutable
class GetActiveHoursResponse {
/// {@macro get_active_hours_response}
const GetActiveHoursResponse({
required this.hourly,
required this.recommendedWindowStartUtc,
required this.recommendedWindowLengthHours,
required this.busiestHourUtc,
required this.lookbackDays,
required this.asOf,
});
/// Converts a `Map<String, dynamic>` to a [GetActiveHoursResponse].
factory GetActiveHoursResponse.fromJson(Map<String, dynamic> json) {
return parseFromJson(
'GetActiveHoursResponse',
json,
() => GetActiveHoursResponse(
hourly: (json['hourly'] as List)
.map<ActiveHourEntry>(
(e) => ActiveHourEntry.fromJson(e as Map<String, dynamic>),
)
.toList(),
recommendedWindowStartUtc:
checkedKey(json, 'recommended_window_start_utc') as int?,
recommendedWindowLengthHours:
json['recommended_window_length_hours'] 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 GetActiveHoursResponse? maybeFromJson(Map<String, dynamic>? json) {
if (json == null) {
return null;
}
return GetActiveHoursResponse.fromJson(json);
}
/// One entry per UTC hour: 24 entries, hour_utc 023, zero-filled,
/// ordered by hour_utc ascending.
final List<ActiveHourEntry> hourly;
/// Start hour (UTC, 023) of the lowest-activity consecutive window —
/// the recommended time to release. The window wraps past midnight.
/// Null when there is insufficient data (fewer than 7 days).
final int? recommendedWindowStartUtc;
/// Length of the recommended window in hours. Fixed at 2 in v1.
final int recommendedWindowLengthHours;
/// UTC hour (023) with the highest average active devices, for
/// contrast in the recommendation copy. Null when insufficient data.
final int? busiestHourUtc;
/// Number of days of history the profile 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 [GetActiveHoursResponse] to a `Map<String, dynamic>`.
Map<String, dynamic> toJson() {
return {
'hourly': hourly.map((e) => e.toJson()).toList(),
'recommended_window_start_utc': recommendedWindowStartUtc,
'recommended_window_length_hours': recommendedWindowLengthHours,
'busiest_hour_utc': busiestHourUtc,
'lookback_days': lookbackDays,
'as_of': asOf.toIso8601String(),
};
}
@override
int get hashCode => Object.hashAll([
listHash(hourly),
recommendedWindowStartUtc,
recommendedWindowLengthHours,
busiestHourUtc,
lookbackDays,
asOf,
]);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is GetActiveHoursResponse &&
listsEqual(hourly, other.hourly) &&
recommendedWindowStartUtc == other.recommendedWindowStartUtc &&
recommendedWindowLengthHours == other.recommendedWindowLengthHours &&
busiestHourUtc == other.busiestHourUtc &&
lookbackDays == other.lookbackDays &&
asOf == other.asOf;
}
}
@@ -0,0 +1,67 @@
import 'package:meta/meta.dart';
import 'package:shorebird_code_push_protocol/model_helpers.dart';
/// {@template active_hour_entry}
/// Average number of distinct active devices during one UTC hour-of-day,
/// averaged across all days in the lookback window (days with no activity
/// in that hour count as zero).
/// {@endtemplate}
@immutable
class ActiveHourEntry {
/// {@macro active_hour_entry}
const ActiveHourEntry({
required this.hourUtc,
required this.averageActiveDevices,
});
/// Converts a `Map<String, dynamic>` to an [ActiveHourEntry].
factory ActiveHourEntry.fromJson(Map<String, dynamic> json) {
return parseFromJson(
'ActiveHourEntry',
json,
() => ActiveHourEntry(
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 ActiveHourEntry? maybeFromJson(Map<String, dynamic>? json) {
if (json == null) {
return null;
}
return ActiveHourEntry.fromJson(json);
}
/// Hour of day in UTC, 023.
final int hourUtc;
/// Mean distinct active devices seen during this UTC hour, averaged
/// over the lookback window with implicit zeros included.
final double averageActiveDevices;
/// Converts an [ActiveHourEntry] to a `Map<String, dynamic>`.
Map<String, dynamic> toJson() {
return {
'hour_utc': hourUtc,
'average_active_devices': averageActiveDevices,
};
}
@override
int get hashCode => Object.hashAll([
hourUtc,
averageActiveDevices,
]);
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ActiveHourEntry &&
hourUtc == other.hourUtc &&
averageActiveDevices == other.averageActiveDevices;
}
}
@@ -0,0 +1,34 @@
// GENERATED — do not hand-edit.
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
import 'package:test/test.dart';
void main() {
group('GetActiveHoursResponse', () {
test('round-trips via maybeFromJson/toJson', () {
final instance = GetActiveHoursResponse(
hourly: const <ActiveHourEntry>[
ActiveHourEntry(hourUtc: 0, averageActiveDevices: 0),
],
recommendedWindowStartUtc: 0,
recommendedWindowLengthHours: 0,
busiestHourUtc: 0,
lookbackDays: 0,
asOf: DateTime.utc(2024),
);
final parsed = GetActiveHoursResponse.maybeFromJson(instance.toJson())!;
expect(parsed, equals(instance));
expect(parsed.hashCode, equals(instance.hashCode));
});
test('maybeFromJson returns null on null input', () {
expect(GetActiveHoursResponse.maybeFromJson(null), isNull);
});
test('maybeFromJson throws FormatException on invalid input', () {
expect(
() => GetActiveHoursResponse.maybeFromJson(<String, dynamic>{}),
throwsFormatException,
);
});
});
}
@@ -0,0 +1,25 @@
// GENERATED — do not hand-edit.
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
import 'package:test/test.dart';
void main() {
group('ActiveHourEntry', () {
test('round-trips via maybeFromJson/toJson', () {
const instance = ActiveHourEntry(hourUtc: 0, averageActiveDevices: 0);
final parsed = ActiveHourEntry.maybeFromJson(instance.toJson())!;
expect(parsed, equals(instance));
expect(parsed.hashCode, equals(instance.hashCode));
});
test('maybeFromJson returns null on null input', () {
expect(ActiveHourEntry.maybeFromJson(null), isNull);
});
test('maybeFromJson throws FormatException on invalid input', () {
expect(
() => ActiveHourEntry.maybeFromJson(<String, dynamic>{}),
throwsFormatException,
);
});
});
}