Skip to content

Commit

Permalink
Add guide for percentage-based rollouts (#312)
Browse files Browse the repository at this point in the history
* Add guide for percentage-based rollouts

* add prefs to cspell

* pre-defined -> predefined

* add reference to v2.0.0
  • Loading branch information
bryanoltman authored Nov 14, 2024
1 parent 1287dd9 commit 82ba698
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"maclinux",
"mipmap",
"Podfile",
"prefs",
"previewable",
"rollouts",
"SDKMAN",
Expand Down
4 changes: 4 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export default defineConfig({
label: 'Crash Reporting',
link: '/guides/crash-reporting',
},
{
label: 'Percentage-Based Rollouts',
link: '/guides/percentage-based-rollouts',
},
{
label: 'Flavors',
autogenerate: {
Expand Down
192 changes: 192 additions & 0 deletions src/content/docs/guides/percentage-based-rollouts.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
---
title: Percentage-Based Rollouts
description: Gradually roll out patches to your users
sidebar:
order: 3
---

This guide describes how you can implement a percentage-based patch rollout
system using predefined tracks.

Our percentage-based rollout system will have the following three parts:

1. Using Shorebird's “tracks” feature to publish patches to different sets of
users.
1. Using a cloud key-value (KV) store to manage rollout percentages for each
release version.
1. Randomly assigning every device a “group number” between 1 and 100. If a
device's group number is less than or equal to the current rollout percentage
for the current app version, that user will get the partially rolled-out
(“beta”) patch. Otherwise, the user will receive the stable patch.

## Add Shorebird to your app

If you haven't already, run shorebird init in your Flutter project to add
Shorebird to your app.

# Add the shorebird_code_push package

This feature also requires v2.0.0 of
[`package:shorebird_code_push`](https://pub.dev/packages/shorebird_code_push).
You can add this to your project using `flutter pub add shorebird_code_push` or
by adding it to your `pubspec.yaml` manually.

You will need to update your `shorebird.yaml` to tell Shorebird you want to
manage your own updates:

```yaml
app_id: your-app-id-here

# Add this line. Setting auto_update to false tells Shorebird not to check
# for stable track updates on app launch

auto_update: false
```
## Add logic to bucket your users
We do this by assigning each user a number between 1 and 100 and saving that
value to a local cache using
[`package:shared_preferences`](https://pub.dev/packages/shared_preferences):

```dart
Future<void> assignGroupNumberIfNeeded() async {
final prefs = await SharedPreferences.getInstance();
if (!prefs.containsKey(groupKey)) {
final groupNumber = Random().nextInt(100) + 1;
await prefs.setInt(groupKey, groupNumber);
}
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await assignGroupNumberIfNeeded();
runApp(const MyApp());
}
```

## Add logic to determine rollout percentage

We've used Firebase's Cloud Firestore to create a simple key-value pairing of
release versions to rollout percentages, although any method of retrieving a
rollout percentage from the cloud will work.

```dart
Future<int?> _fetchRolloutPercentage() async {
final collection = await FirebaseFirestore.instance
.collection(firestoreCollectionName)
.get();
final releaseVersion = await _releaseVersion();
return collection.docs.first.data()[releaseVersion] as int?;
}
```

## Use these values to choose a track

We can now tie this all together by using the group number and the rollout
percentage to decide whether a user should get patches in the beta track or in
the stable track.

```dart
Future<UpdateTrack> _updateTrack() async {
final rolloutPercentage = await _fetchRolloutPercentage();
// If there is no rollout percentage, all users are on the stable track.
if (rolloutPercentage == null) return UpdateTrack.stable;
// If the user's group number is less than or equal to the rollout
// percentage, they are on the beta track. For example:
// - if the rollout percentage is 25%, users with group numbers 1-25
// are on the beta track, and the other 75% are on the stable track.
// - if the rollout percentage is 100%, all users are on the beta track.
return _groupNumber! <= rolloutPercentage
? UpdateTrack.beta
: UpdateTrack.stable;
}
```

## Seeing it in action

We'll start by creating a release for Android:

```
shorebird release android
```

This will create a release build of your app (an aab file) that is patchable
with Shorebird. You will distribute this build to your users the same way you
distribute your apps today.

Now, we'll launch our example using `shorebird preview`, which downloads the
release and runs it on a local device:

![Release build showing group number 76 with a red background](https://github.com/user-attachments/assets/b82c3e48-c2b0-4c91-8c8f-58fe475a4439)

We can see from this screenshot that our device is in group 76. This means that
we will request patches on the stable track until our rollout percentage is >=
76, at which point we will start requesting patches in the beta track.

Because we haven't created a patch on the beta track or set a rollout percentage
yet, there isn't much to see yet.

Let's change the background color from `Colors.deepPurple` to `Colors.red` and
create a patch on the beta track using the following command:

```
shorebird patch android --track=beta
```

And update the rollout percentage in Firebase:

![Cloud Firestore showing 1.0.0+1 at 50% rollout](https://github.com/user-attachments/assets/378eefbc-7299-4d7c-a9d4-7dd08957a596)

Press the update button, and our shorebird preview output shows the following
(formatted for readability):

```
11-13 15:54:06.188 1854 1943 I flutter : updater::network:
[shorebird] Sending patch check request: PatchCheckRequest {
app_id: "fdd48b3f-0b05-41b0-ac22-464600f739ee",
channel: "stable",
release_version: "1.0.0+1",
platform: "android",
arch: "aarch64"
}
```

Now if we change the rollout percentage in Firebase to 76%:

![Cloud Firestore showing 1.0.0+1 at 76% rollout](https://github.com/user-attachments/assets/28adc8ae-8eb2-4770-b79d-1143bc1e2632)

And press the update button again, we will now see:

```
11-13 16:41:42.525 7036 7102 I flutter : updater::network:
[shorebird] Sending patch check request: PatchCheckRequest {
app_id: "fdd48b3f-0b05-41b0-ac22-464600f739ee",
channel: "beta",
release_version: "1.0.0+1",
platform: "android",
arch: "aarch64"
}
11-13 16:41:42.666 7036 7102 I flutter : updater::updater:
[shorebird] Patch check response: PatchCheckResponse {
patch_available: true,
patch: Some(Patch {
number: 1,
hash: "6baa53e40fe1ef0d1230c0ab04ef9dacc7fb5d19368278f2cb2b09a333fdd0c2",
download_url: "https://cdn-dev.shorebird.cloud/api/v1/patches/fdd48b3f-0b05-41b0-ac22-464600f739ee/android/aarch64/3098/dlc.vmcode",
hash_signature: None
}),
rolled_back_patch_numbers: Some([])
}
11-13 16:41:43.279 7036 7102 I flutter : updater::updater:
[shorebird] Patch 1 successfully downloaded. It will be launched when the app next restarts.
```

Note that the channel field in the patch check request has changed from stable
to beta, and that we've downloaded our patch.

![Patched build showing group number 76 with a purple background](https://github.com/user-attachments/assets/cfcc1010-5475-456a-ad25-d9415d1b5ca6)

0 comments on commit 82ba698

Please sign in to comment.