feat(shorebird_code_push_protocol): unique-users current/previous envelope + shared MetricsRange [1/5] (#3821)
This commit is contained in:
@@ -52,6 +52,7 @@ export 'package:shorebird_code_push_protocol/src/models/get_patch_adoption_param
|
||||
export 'package:shorebird_code_push_protocol/src/models/get_unique_users_parameter2.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/get_unique_users_parameter3.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/latest_release.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/metrics_range.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/organization.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/organization_membership.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/organization_type.dart';
|
||||
@@ -59,7 +60,6 @@ export 'package:shorebird_code_push_protocol/src/models/organization_user.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/patch.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/patch_adoption_entry.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/patch_adoption_point.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/patch_adoption_range.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/patch_artifact.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/patch_check_metadata.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/pending_release.dart';
|
||||
@@ -73,8 +73,9 @@ export 'package:shorebird_code_push_protocol/src/models/release_platform.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/release_status.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/role.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/unique_users_breakdown_entry.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/unique_users_range.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/unique_users_current_window.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/unique_users_time_series_entry.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/unique_users_window.dart';
|
||||
export 'package:shorebird_code_push_protocol/src/models/version_distribution_entry.dart';
|
||||
|
||||
/// Parsed JSON data.
|
||||
|
||||
+8
-7
@@ -1,7 +1,7 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/metrics_range.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/patch_adoption_entry.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/patch_adoption_range.dart';
|
||||
|
||||
/// {@template get_patch_adoption_response}
|
||||
/// The response body for GET /apps/{appId}/metrics/patch-adoption. Covers
|
||||
@@ -28,9 +28,7 @@ class GetPatchAdoptionResponse {
|
||||
releaseVersion: json['release_version'] as String,
|
||||
isLatest: json['is_latest'] as bool,
|
||||
granularity: checkedKey(json, 'granularity') as String?,
|
||||
range: PatchAdoptionRange.fromJson(
|
||||
json['range'] as Map<String, dynamic>,
|
||||
),
|
||||
range: MetricsRange.fromJson(json['range'] as Map<String, dynamic>),
|
||||
asOf: DateTime.parse(json['as_of'] as String),
|
||||
patches: (json['patches'] as List)
|
||||
.map<PatchAdoptionEntry>(
|
||||
@@ -57,12 +55,15 @@ class GetPatchAdoptionResponse {
|
||||
/// creation date" default (no `release_version` was supplied).
|
||||
final bool isLatest;
|
||||
|
||||
/// The bucket resolution (`hour`, `day`, `week`, or `month`), or null
|
||||
/// The bucket resolution (`hour`, `day`, or `week`), or null
|
||||
/// when each patch carries a single full-window value.
|
||||
final String? granularity;
|
||||
|
||||
/// The effective (post-clamp) window the response covers.
|
||||
final PatchAdoptionRange range;
|
||||
/// The effective (post-default, post-clamp) window a metrics response —
|
||||
/// or one window of a metrics envelope — covers. Always echoed by the
|
||||
/// server; clients must treat it as authoritative rather than reusing
|
||||
/// the requested range.
|
||||
final MetricsRange range;
|
||||
|
||||
/// Server's UTC timestamp at the moment the response was
|
||||
/// constructed. Not a freshness indicator for the underlying
|
||||
|
||||
+45
-56
@@ -1,22 +1,27 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/unique_users_breakdown_entry.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/unique_users_range.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/unique_users_time_series_entry.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/unique_users_current_window.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/unique_users_window.dart';
|
||||
|
||||
/// {@template get_unique_users_response}
|
||||
/// The response body for GET /apps/{appId}/metrics/unique-users.
|
||||
/// The response body for GET /apps/{appId}/metrics/unique-users: a
|
||||
/// current/previous envelope. `previous` covers the equal-length window
|
||||
/// immediately preceding `current`; period-over-period deltas are client
|
||||
/// display logic over the two totals. `previous` is always present but is
|
||||
/// null when the prior window predates the data floor (no comparison data
|
||||
/// exists). When it reaches past the plan's metrics-history horizon,
|
||||
/// `previous` is non-null with its total — the delta renders — but without
|
||||
/// a `time_series` (no prior-window overlay): granular history is the
|
||||
/// resolution the horizon gates, the scalar comparison is not.
|
||||
/// {@endtemplate}
|
||||
@immutable
|
||||
class GetUniqueUsersResponse {
|
||||
/// {@macro get_unique_users_response}
|
||||
const GetUniqueUsersResponse({
|
||||
required this.uniqueUsers,
|
||||
required this.granularity,
|
||||
required this.range,
|
||||
required this.asOf,
|
||||
this.timeSeries,
|
||||
this.breakdown,
|
||||
required this.granularity,
|
||||
required this.current,
|
||||
required this.previous,
|
||||
});
|
||||
|
||||
/// Converts a `Map<String, dynamic>` to a [GetUniqueUsersResponse].
|
||||
@@ -25,23 +30,14 @@ class GetUniqueUsersResponse {
|
||||
'GetUniqueUsersResponse',
|
||||
json,
|
||||
() => GetUniqueUsersResponse(
|
||||
uniqueUsers: json['unique_users'] as int,
|
||||
granularity: checkedKey(json, 'granularity') as String?,
|
||||
range: UniqueUsersRange.fromJson(json['range'] as Map<String, dynamic>),
|
||||
asOf: DateTime.parse(json['as_of'] as String),
|
||||
timeSeries: (json['time_series'] as List?)
|
||||
?.map<UniqueUsersTimeSeriesEntry>(
|
||||
(e) => UniqueUsersTimeSeriesEntry.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
breakdown: (json['breakdown'] as List?)
|
||||
?.map<UniqueUsersBreakdownEntry>(
|
||||
(e) =>
|
||||
UniqueUsersBreakdownEntry.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
granularity: checkedKey(json, 'granularity') as String?,
|
||||
current: UniqueUsersCurrentWindow.fromJson(
|
||||
json['current'] as Map<String, dynamic>,
|
||||
),
|
||||
previous: UniqueUsersWindow.maybeFromJson(
|
||||
checkedKey(json, 'previous') as Map<String, dynamic>?,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -55,60 +51,53 @@ class GetUniqueUsersResponse {
|
||||
return GetUniqueUsersResponse.fromJson(json);
|
||||
}
|
||||
|
||||
/// Distinct active devices over the whole window (an HLL count).
|
||||
final int uniqueUsers;
|
||||
|
||||
/// The time-series bucket resolution (`hour`, `day`, `week`, or
|
||||
/// `month`), or null when no time series was requested.
|
||||
final String? granularity;
|
||||
|
||||
/// The window the unique-users response covers.
|
||||
final UniqueUsersRange range;
|
||||
|
||||
/// 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;
|
||||
|
||||
/// The top-level time series, present only when a `granularity` was
|
||||
/// requested; otherwise null.
|
||||
final List<UniqueUsersTimeSeriesEntry>? timeSeries;
|
||||
/// The time-series bucket resolution (`hour`, `day`, or `week`), or
|
||||
/// null when no time series was requested. Applies to both windows.
|
||||
final String? granularity;
|
||||
|
||||
/// Per-group unique users, present only when a `group_by` was
|
||||
/// requested; otherwise null.
|
||||
final List<UniqueUsersBreakdownEntry>? breakdown;
|
||||
/// The `current` window of the unique-users envelope: the base window
|
||||
/// atom plus the optional `breakdown`. Only `current` carries a
|
||||
/// breakdown — no chart renders a previous-window breakdown, so the
|
||||
/// asymmetry is declared in the contract rather than left as an
|
||||
/// optional-but-never-populated field.
|
||||
final UniqueUsersCurrentWindow current;
|
||||
|
||||
/// One window of the unique-users envelope: the HLL-merged total over
|
||||
/// the window's effective range, with a per-bucket series when a
|
||||
/// `granularity` was requested. This base atom is the full shape of
|
||||
/// `previous`; `current` extends it (see UniqueUsersCurrentWindow).
|
||||
final UniqueUsersWindow? previous;
|
||||
|
||||
/// Converts a [GetUniqueUsersResponse] to a `Map<String, dynamic>`.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'unique_users': uniqueUsers,
|
||||
'granularity': granularity,
|
||||
'range': range.toJson(),
|
||||
'as_of': asOf.toIso8601String(),
|
||||
'time_series': timeSeries?.map((e) => e.toJson()).toList(),
|
||||
'breakdown': breakdown?.map((e) => e.toJson()).toList(),
|
||||
'granularity': granularity,
|
||||
'current': current.toJson(),
|
||||
'previous': previous?.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([
|
||||
uniqueUsers,
|
||||
granularity,
|
||||
range,
|
||||
asOf,
|
||||
listHash(timeSeries),
|
||||
listHash(breakdown),
|
||||
granularity,
|
||||
current,
|
||||
previous,
|
||||
]);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is GetUniqueUsersResponse &&
|
||||
uniqueUsers == other.uniqueUsers &&
|
||||
granularity == other.granularity &&
|
||||
range == other.range &&
|
||||
asOf == other.asOf &&
|
||||
listsEqual(timeSeries, other.timeSeries) &&
|
||||
listsEqual(breakdown, other.breakdown);
|
||||
granularity == other.granularity &&
|
||||
current == other.current &&
|
||||
previous == other.previous;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
enum GetPatchAdoptionParameter3 {
|
||||
hour._('hour'),
|
||||
day._('day'),
|
||||
week._('week'),
|
||||
month._('month');
|
||||
week._('week');
|
||||
|
||||
const GetPatchAdoptionParameter3._(this.value);
|
||||
|
||||
|
||||
+1
-2
@@ -1,8 +1,7 @@
|
||||
enum GetUniqueUsersParameter2 {
|
||||
hour._('hour'),
|
||||
day._('day'),
|
||||
week._('week'),
|
||||
month._('month');
|
||||
week._('week');
|
||||
|
||||
const GetUniqueUsersParameter2._(this.value);
|
||||
|
||||
|
||||
+16
-15
@@ -1,23 +1,26 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||
|
||||
/// {@template patch_adoption_range}
|
||||
/// The effective (post-clamp) window the response covers.
|
||||
/// {@template metrics_range}
|
||||
/// The effective (post-default, post-clamp) window a metrics response —
|
||||
/// or one window of a metrics envelope — covers. Always echoed by the
|
||||
/// server; clients must treat it as authoritative rather than reusing
|
||||
/// the requested range.
|
||||
/// {@endtemplate}
|
||||
@immutable
|
||||
class PatchAdoptionRange {
|
||||
/// {@macro patch_adoption_range}
|
||||
const PatchAdoptionRange({
|
||||
class MetricsRange {
|
||||
/// {@macro metrics_range}
|
||||
const MetricsRange({
|
||||
required this.start,
|
||||
required this.end,
|
||||
});
|
||||
|
||||
/// Converts a `Map<String, dynamic>` to a [PatchAdoptionRange].
|
||||
factory PatchAdoptionRange.fromJson(Map<String, dynamic> json) {
|
||||
/// Converts a `Map<String, dynamic>` to a [MetricsRange].
|
||||
factory MetricsRange.fromJson(Map<String, dynamic> json) {
|
||||
return parseFromJson(
|
||||
'PatchAdoptionRange',
|
||||
'MetricsRange',
|
||||
json,
|
||||
() => PatchAdoptionRange(
|
||||
() => MetricsRange(
|
||||
start: DateTime.parse(json['start'] as String),
|
||||
end: DateTime.parse(json['end'] as String),
|
||||
),
|
||||
@@ -26,11 +29,11 @@ class PatchAdoptionRange {
|
||||
|
||||
/// Convenience to create a nullable type from a nullable json object.
|
||||
/// Useful when parsing optional fields.
|
||||
static PatchAdoptionRange? maybeFromJson(Map<String, dynamic>? json) {
|
||||
static MetricsRange? maybeFromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return PatchAdoptionRange.fromJson(json);
|
||||
return MetricsRange.fromJson(json);
|
||||
}
|
||||
|
||||
/// Window start (UTC, inclusive).
|
||||
@@ -39,7 +42,7 @@ class PatchAdoptionRange {
|
||||
/// Window end (UTC, exclusive).
|
||||
final DateTime end;
|
||||
|
||||
/// Converts a [PatchAdoptionRange] to a `Map<String, dynamic>`.
|
||||
/// Converts a [MetricsRange] to a `Map<String, dynamic>`.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'start': start.toIso8601String(),
|
||||
@@ -56,8 +59,6 @@ class PatchAdoptionRange {
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is PatchAdoptionRange &&
|
||||
start == other.start &&
|
||||
end == other.end;
|
||||
return other is MetricsRange && start == other.start && end == other.end;
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/metrics_range.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/unique_users_breakdown_entry.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/unique_users_time_series_entry.dart';
|
||||
|
||||
/// {@template unique_users_current_window}
|
||||
/// The `current` window of the unique-users envelope: the base window
|
||||
/// atom plus the optional `breakdown`. Only `current` carries a
|
||||
/// breakdown — no chart renders a previous-window breakdown, so the
|
||||
/// asymmetry is declared in the contract rather than left as an
|
||||
/// optional-but-never-populated field.
|
||||
/// {@endtemplate}
|
||||
@immutable
|
||||
class UniqueUsersCurrentWindow {
|
||||
/// {@macro unique_users_current_window}
|
||||
const UniqueUsersCurrentWindow({
|
||||
required this.uniqueUsers,
|
||||
required this.range,
|
||||
this.timeSeries,
|
||||
this.breakdown,
|
||||
});
|
||||
|
||||
/// Converts a `Map<String, dynamic>` to a [UniqueUsersCurrentWindow].
|
||||
factory UniqueUsersCurrentWindow.fromJson(Map<String, dynamic> json) {
|
||||
return parseFromJson(
|
||||
'UniqueUsersCurrentWindow',
|
||||
json,
|
||||
() => UniqueUsersCurrentWindow(
|
||||
uniqueUsers: json['unique_users'] as int,
|
||||
range: MetricsRange.fromJson(json['range'] as Map<String, dynamic>),
|
||||
timeSeries: (json['time_series'] as List?)
|
||||
?.map<UniqueUsersTimeSeriesEntry>(
|
||||
(e) => UniqueUsersTimeSeriesEntry.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
breakdown: (json['breakdown'] as List?)
|
||||
?.map<UniqueUsersBreakdownEntry>(
|
||||
(e) =>
|
||||
UniqueUsersBreakdownEntry.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convenience to create a nullable type from a nullable json object.
|
||||
/// Useful when parsing optional fields.
|
||||
static UniqueUsersCurrentWindow? maybeFromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return UniqueUsersCurrentWindow.fromJson(json);
|
||||
}
|
||||
|
||||
/// Distinct active devices over this window (an HLL count). Note:
|
||||
/// per-bucket `time_series` values do not sum to this — an HLL merge
|
||||
/// over the window is not a sum of per-bucket merges.
|
||||
final int uniqueUsers;
|
||||
|
||||
/// The effective (post-default, post-clamp) window a metrics response —
|
||||
/// or one window of a metrics envelope — covers. Always echoed by the
|
||||
/// server; clients must treat it as authoritative rather than reusing
|
||||
/// the requested range.
|
||||
final MetricsRange range;
|
||||
|
||||
/// Per-bucket series for this window, present only when a
|
||||
/// `granularity` was requested; otherwise null. On `previous`, also
|
||||
/// null when the prior window reaches past the plan's metrics-history
|
||||
/// horizon (the total is still present — only the granular overlay is
|
||||
/// withheld). Empty buckets are omitted — gap-fill against this
|
||||
/// window's `range`.
|
||||
final List<UniqueUsersTimeSeriesEntry>? timeSeries;
|
||||
|
||||
/// Per-group unique users for this window, present only when a
|
||||
/// `group_by` was requested; otherwise null.
|
||||
final List<UniqueUsersBreakdownEntry>? breakdown;
|
||||
|
||||
/// Converts a [UniqueUsersCurrentWindow] to a `Map<String, dynamic>`.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'unique_users': uniqueUsers,
|
||||
'range': range.toJson(),
|
||||
'time_series': timeSeries?.map((e) => e.toJson()).toList(),
|
||||
'breakdown': breakdown?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([
|
||||
uniqueUsers,
|
||||
range,
|
||||
listHash(timeSeries),
|
||||
listHash(breakdown),
|
||||
]);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is UniqueUsersCurrentWindow &&
|
||||
uniqueUsers == other.uniqueUsers &&
|
||||
range == other.range &&
|
||||
listsEqual(timeSeries, other.timeSeries) &&
|
||||
listsEqual(breakdown, other.breakdown);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||
|
||||
/// {@template unique_users_range}
|
||||
/// The window the unique-users response covers.
|
||||
/// {@endtemplate}
|
||||
@immutable
|
||||
class UniqueUsersRange {
|
||||
/// {@macro unique_users_range}
|
||||
const UniqueUsersRange({
|
||||
required this.start,
|
||||
required this.end,
|
||||
});
|
||||
|
||||
/// Converts a `Map<String, dynamic>` to a [UniqueUsersRange].
|
||||
factory UniqueUsersRange.fromJson(Map<String, dynamic> json) {
|
||||
return parseFromJson(
|
||||
'UniqueUsersRange',
|
||||
json,
|
||||
() => UniqueUsersRange(
|
||||
start: DateTime.parse(json['start'] as String),
|
||||
end: DateTime.parse(json['end'] as String),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convenience to create a nullable type from a nullable json object.
|
||||
/// Useful when parsing optional fields.
|
||||
static UniqueUsersRange? maybeFromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return UniqueUsersRange.fromJson(json);
|
||||
}
|
||||
|
||||
/// Window start (UTC, inclusive).
|
||||
final DateTime start;
|
||||
|
||||
/// Window end (UTC, exclusive).
|
||||
final DateTime end;
|
||||
|
||||
/// Converts a [UniqueUsersRange] to a `Map<String, dynamic>`.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'start': start.toIso8601String(),
|
||||
'end': end.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([
|
||||
start,
|
||||
end,
|
||||
]);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is UniqueUsersRange &&
|
||||
start == other.start &&
|
||||
end == other.end;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:shorebird_code_push_protocol/model_helpers.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/metrics_range.dart';
|
||||
import 'package:shorebird_code_push_protocol/src/models/unique_users_time_series_entry.dart';
|
||||
|
||||
/// {@template unique_users_window}
|
||||
/// One window of the unique-users envelope: the HLL-merged total over
|
||||
/// the window's effective range, with a per-bucket series when a
|
||||
/// `granularity` was requested. This base atom is the full shape of
|
||||
/// `previous`; `current` extends it (see UniqueUsersCurrentWindow).
|
||||
/// {@endtemplate}
|
||||
@immutable
|
||||
class UniqueUsersWindow {
|
||||
/// {@macro unique_users_window}
|
||||
const UniqueUsersWindow({
|
||||
required this.uniqueUsers,
|
||||
required this.range,
|
||||
this.timeSeries,
|
||||
});
|
||||
|
||||
/// Converts a `Map<String, dynamic>` to a [UniqueUsersWindow].
|
||||
factory UniqueUsersWindow.fromJson(Map<String, dynamic> json) {
|
||||
return parseFromJson(
|
||||
'UniqueUsersWindow',
|
||||
json,
|
||||
() => UniqueUsersWindow(
|
||||
uniqueUsers: json['unique_users'] as int,
|
||||
range: MetricsRange.fromJson(json['range'] as Map<String, dynamic>),
|
||||
timeSeries: (json['time_series'] as List?)
|
||||
?.map<UniqueUsersTimeSeriesEntry>(
|
||||
(e) => UniqueUsersTimeSeriesEntry.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convenience to create a nullable type from a nullable json object.
|
||||
/// Useful when parsing optional fields.
|
||||
static UniqueUsersWindow? maybeFromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
return UniqueUsersWindow.fromJson(json);
|
||||
}
|
||||
|
||||
/// Distinct active devices over this window (an HLL count). Note:
|
||||
/// per-bucket `time_series` values do not sum to this — an HLL merge
|
||||
/// over the window is not a sum of per-bucket merges.
|
||||
final int uniqueUsers;
|
||||
|
||||
/// The effective (post-default, post-clamp) window a metrics response —
|
||||
/// or one window of a metrics envelope — covers. Always echoed by the
|
||||
/// server; clients must treat it as authoritative rather than reusing
|
||||
/// the requested range.
|
||||
final MetricsRange range;
|
||||
|
||||
/// Per-bucket series for this window, present only when a
|
||||
/// `granularity` was requested; otherwise null. On `previous`, also
|
||||
/// null when the prior window reaches past the plan's metrics-history
|
||||
/// horizon (the total is still present — only the granular overlay is
|
||||
/// withheld). Empty buckets are omitted — gap-fill against this
|
||||
/// window's `range`.
|
||||
final List<UniqueUsersTimeSeriesEntry>? timeSeries;
|
||||
|
||||
/// Converts a [UniqueUsersWindow] to a `Map<String, dynamic>`.
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'unique_users': uniqueUsers,
|
||||
'range': range.toJson(),
|
||||
'time_series': timeSeries?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([
|
||||
uniqueUsers,
|
||||
range,
|
||||
listHash(timeSeries),
|
||||
]);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
return other is UniqueUsersWindow &&
|
||||
uniqueUsers == other.uniqueUsers &&
|
||||
range == other.range &&
|
||||
listsEqual(timeSeries, other.timeSeries);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -9,7 +9,7 @@ void main() {
|
||||
releaseVersion: 'example',
|
||||
isLatest: false,
|
||||
granularity: 'example',
|
||||
range: PatchAdoptionRange(
|
||||
range: MetricsRange(
|
||||
start: DateTime.utc(2024),
|
||||
end: DateTime.utc(2024),
|
||||
),
|
||||
|
||||
+16
-7
@@ -6,15 +6,24 @@ void main() {
|
||||
group('GetUniqueUsersResponse', () {
|
||||
test('round-trips via maybeFromJson/toJson', () {
|
||||
final instance = GetUniqueUsersResponse(
|
||||
uniqueUsers: 0,
|
||||
granularity: 'example',
|
||||
range: UniqueUsersRange(
|
||||
start: DateTime.utc(2024),
|
||||
end: DateTime.utc(2024),
|
||||
),
|
||||
asOf: DateTime.utc(2024),
|
||||
granularity: 'example',
|
||||
current: UniqueUsersCurrentWindow(
|
||||
uniqueUsers: 0,
|
||||
range: MetricsRange(
|
||||
start: DateTime.utc(2024),
|
||||
end: DateTime.utc(2024),
|
||||
),
|
||||
),
|
||||
previous: UniqueUsersWindow(
|
||||
uniqueUsers: 0,
|
||||
range: MetricsRange(
|
||||
start: DateTime.utc(2024),
|
||||
end: DateTime.utc(2024),
|
||||
),
|
||||
),
|
||||
);
|
||||
final parsed = GetUniqueUsersResponse.maybeFromJson(instance.toJson());
|
||||
final parsed = GetUniqueUsersResponse.maybeFromJson(instance.toJson())!;
|
||||
expect(parsed, equals(instance));
|
||||
expect(parsed.hashCode, equals(instance.hashCode));
|
||||
});
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ void main() {
|
||||
group('GetUniqueUsersParameter2', () {
|
||||
test('round-trips via maybeFromJson/toJson', () {
|
||||
final instance = GetUniqueUsersParameter2.values.first;
|
||||
final parsed = GetUniqueUsersParameter2.maybeFromJson(instance.toJson());
|
||||
final parsed = GetUniqueUsersParameter2.maybeFromJson(instance.toJson())!;
|
||||
expect(parsed, equals(instance));
|
||||
expect(parsed.hashCode, equals(instance.hashCode));
|
||||
});
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ void main() {
|
||||
group('GetUniqueUsersParameter3', () {
|
||||
test('round-trips via maybeFromJson/toJson', () {
|
||||
final instance = GetUniqueUsersParameter3.values.first;
|
||||
final parsed = GetUniqueUsersParameter3.maybeFromJson(instance.toJson());
|
||||
final parsed = GetUniqueUsersParameter3.maybeFromJson(instance.toJson())!;
|
||||
expect(parsed, equals(instance));
|
||||
expect(parsed.hashCode, equals(instance.hashCode));
|
||||
});
|
||||
|
||||
+5
-5
@@ -3,24 +3,24 @@ import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('UniqueUsersRange', () {
|
||||
group('MetricsRange', () {
|
||||
test('round-trips via maybeFromJson/toJson', () {
|
||||
final instance = UniqueUsersRange(
|
||||
final instance = MetricsRange(
|
||||
start: DateTime.utc(2024),
|
||||
end: DateTime.utc(2024),
|
||||
);
|
||||
final parsed = UniqueUsersRange.maybeFromJson(instance.toJson());
|
||||
final parsed = MetricsRange.maybeFromJson(instance.toJson())!;
|
||||
expect(parsed, equals(instance));
|
||||
expect(parsed.hashCode, equals(instance.hashCode));
|
||||
});
|
||||
|
||||
test('maybeFromJson returns null on null input', () {
|
||||
expect(UniqueUsersRange.maybeFromJson(null), isNull);
|
||||
expect(MetricsRange.maybeFromJson(null), isNull);
|
||||
});
|
||||
|
||||
test('maybeFromJson throws FormatException on invalid input', () {
|
||||
expect(
|
||||
() => UniqueUsersRange.maybeFromJson(<String, dynamic>{}),
|
||||
() => MetricsRange.maybeFromJson(<String, dynamic>{}),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
+2
-2
@@ -5,14 +5,14 @@ import 'package:test/test.dart';
|
||||
void main() {
|
||||
group('UniqueUsersBreakdownEntry', () {
|
||||
test('round-trips via maybeFromJson/toJson', () {
|
||||
final instance = UniqueUsersBreakdownEntry(
|
||||
const instance = UniqueUsersBreakdownEntry(
|
||||
groupBy: 'example',
|
||||
groupValue: 'example',
|
||||
uniqueUsers: 0,
|
||||
);
|
||||
final parsed = UniqueUsersBreakdownEntry.maybeFromJson(
|
||||
instance.toJson(),
|
||||
);
|
||||
)!;
|
||||
expect(parsed, equals(instance));
|
||||
expect(parsed.hashCode, equals(instance.hashCode));
|
||||
});
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// GENERATED — do not hand-edit.
|
||||
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('UniqueUsersCurrentWindow', () {
|
||||
test('round-trips via maybeFromJson/toJson', () {
|
||||
final instance = UniqueUsersCurrentWindow(
|
||||
uniqueUsers: 0,
|
||||
range: MetricsRange(
|
||||
start: DateTime.utc(2024),
|
||||
end: DateTime.utc(2024),
|
||||
),
|
||||
);
|
||||
final parsed = UniqueUsersCurrentWindow.maybeFromJson(instance.toJson())!;
|
||||
expect(parsed, equals(instance));
|
||||
expect(parsed.hashCode, equals(instance.hashCode));
|
||||
});
|
||||
|
||||
test('maybeFromJson returns null on null input', () {
|
||||
expect(UniqueUsersCurrentWindow.maybeFromJson(null), isNull);
|
||||
});
|
||||
|
||||
test('maybeFromJson throws FormatException on invalid input', () {
|
||||
expect(
|
||||
() => UniqueUsersCurrentWindow.maybeFromJson(<String, dynamic>{}),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ void main() {
|
||||
);
|
||||
final parsed = UniqueUsersTimeSeriesEntry.maybeFromJson(
|
||||
instance.toJson(),
|
||||
);
|
||||
)!;
|
||||
expect(parsed, equals(instance));
|
||||
expect(parsed.hashCode, equals(instance.hashCode));
|
||||
});
|
||||
|
||||
+10
-7
@@ -3,24 +3,27 @@ import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('PatchAdoptionRange', () {
|
||||
group('UniqueUsersWindow', () {
|
||||
test('round-trips via maybeFromJson/toJson', () {
|
||||
final instance = PatchAdoptionRange(
|
||||
start: DateTime.utc(2024),
|
||||
end: DateTime.utc(2024),
|
||||
final instance = UniqueUsersWindow(
|
||||
uniqueUsers: 0,
|
||||
range: MetricsRange(
|
||||
start: DateTime.utc(2024),
|
||||
end: DateTime.utc(2024),
|
||||
),
|
||||
);
|
||||
final parsed = PatchAdoptionRange.maybeFromJson(instance.toJson())!;
|
||||
final parsed = UniqueUsersWindow.maybeFromJson(instance.toJson())!;
|
||||
expect(parsed, equals(instance));
|
||||
expect(parsed.hashCode, equals(instance.hashCode));
|
||||
});
|
||||
|
||||
test('maybeFromJson returns null on null input', () {
|
||||
expect(PatchAdoptionRange.maybeFromJson(null), isNull);
|
||||
expect(UniqueUsersWindow.maybeFromJson(null), isNull);
|
||||
});
|
||||
|
||||
test('maybeFromJson throws FormatException on invalid input', () {
|
||||
expect(
|
||||
() => PatchAdoptionRange.maybeFromJson(<String, dynamic>{}),
|
||||
() => UniqueUsersWindow.maybeFromJson(<String, dynamic>{}),
|
||||
throwsFormatException,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
// The generated round-trip tests cover the required-fields-only case;
|
||||
// these cover the fully-populated window atoms — in particular the
|
||||
// allOf-composed current window carrying both `time_series` and
|
||||
// `breakdown` — and the envelope that nests them.
|
||||
final range = MetricsRange(
|
||||
start: DateTime.utc(2026, 4, 22),
|
||||
end: DateTime.utc(2026, 5, 20),
|
||||
);
|
||||
final series = [
|
||||
UniqueUsersTimeSeriesEntry(
|
||||
period: DateTime.utc(2026, 4, 22),
|
||||
uniqueUsers: 10,
|
||||
),
|
||||
UniqueUsersTimeSeriesEntry(
|
||||
period: DateTime.utc(2026, 4, 23),
|
||||
uniqueUsers: 12,
|
||||
),
|
||||
];
|
||||
|
||||
group('UniqueUsersWindow', () {
|
||||
test('round-trips with a populated time_series', () {
|
||||
final window = UniqueUsersWindow(
|
||||
uniqueUsers: 42,
|
||||
range: range,
|
||||
timeSeries: series,
|
||||
);
|
||||
final parsed = UniqueUsersWindow.fromJson(window.toJson());
|
||||
expect(parsed, equals(window));
|
||||
expect(parsed.toJson(), equals(window.toJson()));
|
||||
});
|
||||
});
|
||||
|
||||
group('UniqueUsersCurrentWindow', () {
|
||||
test('round-trips with time_series and breakdown populated', () {
|
||||
final window = UniqueUsersCurrentWindow(
|
||||
uniqueUsers: 42,
|
||||
range: range,
|
||||
timeSeries: series,
|
||||
breakdown: [
|
||||
UniqueUsersBreakdownEntry(
|
||||
groupBy: 'platform',
|
||||
groupValue: 'android',
|
||||
uniqueUsers: 30,
|
||||
timeSeries: series,
|
||||
),
|
||||
const UniqueUsersBreakdownEntry(
|
||||
groupBy: 'platform',
|
||||
groupValue: 'ios',
|
||||
uniqueUsers: 12,
|
||||
),
|
||||
],
|
||||
);
|
||||
final parsed = UniqueUsersCurrentWindow.fromJson(window.toJson());
|
||||
expect(parsed, equals(window));
|
||||
expect(parsed.toJson(), equals(window.toJson()));
|
||||
});
|
||||
});
|
||||
|
||||
group('GetUniqueUsersResponse', () {
|
||||
test('round-trips the full current/previous envelope', () {
|
||||
final response = GetUniqueUsersResponse(
|
||||
asOf: DateTime.utc(2026, 5, 20, 17, 30),
|
||||
granularity: 'day',
|
||||
current: UniqueUsersCurrentWindow(
|
||||
uniqueUsers: 42,
|
||||
range: range,
|
||||
timeSeries: series,
|
||||
breakdown: [
|
||||
UniqueUsersBreakdownEntry(
|
||||
groupBy: 'platform',
|
||||
groupValue: 'android',
|
||||
uniqueUsers: 30,
|
||||
timeSeries: series,
|
||||
),
|
||||
],
|
||||
),
|
||||
previous: UniqueUsersWindow(
|
||||
uniqueUsers: 37,
|
||||
range: MetricsRange(
|
||||
start: DateTime.utc(2026, 3, 25),
|
||||
end: DateTime.utc(2026, 4, 22),
|
||||
),
|
||||
timeSeries: series,
|
||||
),
|
||||
);
|
||||
final parsed = GetUniqueUsersResponse.fromJson(response.toJson());
|
||||
expect(parsed, equals(response));
|
||||
expect(parsed.toJson(), equals(response.toJson()));
|
||||
});
|
||||
|
||||
test('round-trips the no-granularity envelope (null sections)', () {
|
||||
final response = GetUniqueUsersResponse(
|
||||
asOf: DateTime.utc(2026, 5, 20, 17, 30),
|
||||
granularity: null,
|
||||
current: UniqueUsersCurrentWindow(uniqueUsers: 42, range: range),
|
||||
previous: UniqueUsersWindow(
|
||||
uniqueUsers: 37,
|
||||
range: MetricsRange(
|
||||
start: DateTime.utc(2026, 3, 25),
|
||||
end: DateTime.utc(2026, 4, 22),
|
||||
),
|
||||
),
|
||||
);
|
||||
final parsed = GetUniqueUsersResponse.fromJson(response.toJson());
|
||||
expect(parsed, equals(response));
|
||||
expect(parsed.current.timeSeries, isNull);
|
||||
expect(parsed.current.breakdown, isNull);
|
||||
expect(parsed.previous!.timeSeries, isNull);
|
||||
});
|
||||
|
||||
test('round-trips an envelope with previous null', () {
|
||||
// `previous` is always present but null when the prior window predates
|
||||
// the data floor (no comparison data); the key is still emitted, so it
|
||||
// is null on the wire and after a round-trip.
|
||||
final response = GetUniqueUsersResponse(
|
||||
asOf: DateTime.utc(2026, 5, 20, 17, 30),
|
||||
granularity: null,
|
||||
current: UniqueUsersCurrentWindow(uniqueUsers: 42, range: range),
|
||||
previous: null,
|
||||
);
|
||||
final json = response.toJson();
|
||||
expect(json['previous'], isNull);
|
||||
final parsed = GetUniqueUsersResponse.fromJson(json);
|
||||
expect(parsed, equals(response));
|
||||
expect(parsed.previous, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user