Skip to content

Commit fbb5f61

Browse files
authored
Merge pull request #364 from wordpress-mobile/app-size-metrics
Implement App size metrics actions
2 parents a4d04a4 + b3f47b4 commit fbb5f61

12 files changed

+3014
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ _None_
1010

1111
### New Features
1212

13-
_None_
13+
* Introduce new `ios_send_app_size_metrics` and `android_send_app_size_metrics` actions. [#364]
1414

1515
### Bug Fixes
1616

Gemfile.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ PATH
1212
nokogiri (~> 1.11)
1313
octokit (~> 4.18)
1414
parallel (~> 1.14)
15+
plist (~> 3.1)
1516
progress_bar (~> 1.3)
1617
rake (>= 12.3, < 14.0)
1718
rake-compiler (~> 1.0)
@@ -427,4 +428,4 @@ DEPENDENCIES
427428
yard
428429

429430
BUNDLED WITH
430-
2.3.11
431+
2.3.13

fastlane-plugin-wpmreleasetoolkit.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
2929
spec.add_dependency 'nokogiri', '~> 1.11' # Needed for AndroidLocalizeHelper
3030
spec.add_dependency 'octokit', '~> 4.18'
3131
spec.add_dependency 'buildkit', '~> 1.5'
32+
spec.add_dependency 'plist', '~> 3.1'
3233
spec.add_dependency 'git', '~> 1.3'
3334
spec.add_dependency 'jsonlint', '~> 0.3'
3435
spec.add_dependency 'rake', '>= 12.3', '< 14.0'
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
require_relative '../../helper/app_size_metrics_helper'
2+
3+
module Fastlane
4+
module Actions
5+
class AndroidSendAppSizeMetricsAction < Action
6+
def self.run(params)
7+
# Check input parameters
8+
api_url = URI(params[:api_url])
9+
api_token = params[:api_token]
10+
if (api_token.nil? || api_token.empty?) && !api_url.is_a?(URI::File)
11+
UI.user_error!('An API token is required when using an `api_url` with a scheme other than `file://`')
12+
end
13+
14+
# Build the payload base
15+
metrics_helper = Fastlane::Helper::AppSizeMetricsHelper.new(
16+
Platform: 'Android',
17+
'App Name': params[:app_name],
18+
'App Version': params[:app_version_name],
19+
'Version Code': params[:app_version_code],
20+
'Product Flavor': params[:product_flavor],
21+
'Build Type': params[:build_type],
22+
Source: params[:source]
23+
)
24+
metrics_helper.add_metric(name: 'AAB File Size', value: File.size(params[:aab_path]))
25+
26+
# Add device-specific 'splits' metrics to the payload if a `:include_split_sizes` is enabled
27+
if params[:include_split_sizes]
28+
check_bundletool_installed!
29+
apkanalyzer_bin = params[:apkanalyzer_binary] || find_apkanalyzer_binary!
30+
UI.message("[App Size Metrics] Generating the various APK splits from #{params[:aab_path]}...")
31+
Dir.mktmpdir('release-toolkit-android-app-size-metrics') do |tmp_dir|
32+
Action.sh('bundletool', 'build-apks', '--bundle', params[:aab_path], '--output-format', 'DIRECTORY', '--output', tmp_dir)
33+
apks = Dir.glob('splits/*.apk', base: tmp_dir).map { |f| File.join(tmp_dir, f) }
34+
UI.message("[App Size Metrics] Generated #{apks.length} APKs.")
35+
36+
apks.each do |apk|
37+
UI.message("[App Size Metrics] Computing file and download size of #{File.basename(apk)}...")
38+
split_name = File.basename(apk, '.apk')
39+
file_size = Action.sh(apkanalyzer_bin, 'apk', 'file-size', apk, print_command: false, print_command_output: false).chomp.to_i
40+
download_size = Action.sh(apkanalyzer_bin, 'apk', 'download-size', apk, print_command: false, print_command_output: false).chomp.to_i
41+
metrics_helper.add_metric(name: 'APK File Size', value: file_size, metadata: { split: split_name })
42+
metrics_helper.add_metric(name: 'Download Size', value: download_size, metadata: { split: split_name })
43+
end
44+
45+
UI.message('[App Size Metrics] Done computing splits sizes.')
46+
end
47+
end
48+
49+
# Send the payload
50+
metrics_helper.send_metrics(
51+
to: api_url,
52+
api_token: api_token,
53+
use_gzip: params[:use_gzip_content_encoding]
54+
)
55+
end
56+
57+
def self.check_bundletool_installed!
58+
Action.sh('command', '-v', 'bundletool', print_command: false, print_command_output: false)
59+
rescue StandardError
60+
UI.user_error!('bundletool is required to build the split APKs. Install it with `brew install bundletool`')
61+
raise
62+
end
63+
64+
def self.find_apkanalyzer_binary
65+
sdk_root = ENV['ANDROID_SDK_ROOT'] || ENV['ANDROID_HOME']
66+
if sdk_root
67+
pattern = File.join(sdk_root, 'cmdline-tools', '{latest,tools}', 'bin', 'apkanalyzer')
68+
apkanalyzer_bin = Dir.glob(pattern).find { |path| File.executable?(path) }
69+
end
70+
apkanalyzer_bin || Action.sh('command', '-v', 'apkanalyzer', print_command_output: false) { |_| nil }
71+
end
72+
73+
def self.find_apkanalyzer_binary!
74+
apkanalyzer_bin = find_apkanalyzer_binary
75+
UI.user_error!('Unable to find `apkanalyzer` executable in `$PATH` nor `$ANDROID_SDK_ROOT`. Make sure you installed the Android SDK Command-line Tools') if apkanalyzer_bin.nil?
76+
apkanalyzer_bin
77+
end
78+
79+
#####################################################
80+
# @!group Documentation
81+
#####################################################
82+
83+
def self.description
84+
'Send Android app size metrics to our metrics server'
85+
end
86+
87+
def self.details
88+
<<~DETAILS
89+
Send Android app size metrics to our metrics server.
90+
91+
See https://github.com/Automattic/apps-metrics for the API contract expected by the Metrics server you will send those metrics to.
92+
93+
Tip: If you provide a `file://` URL for the `api_url`, the action will write the payload on disk at the specified path instead of sending
94+
the data to a endpoint over network. This can be useful e.g. to inspect the payload and debug it, or to store the metrics data as CI artefacts.
95+
DETAILS
96+
end
97+
98+
def self.available_options
99+
[
100+
FastlaneCore::ConfigItem.new(
101+
key: :api_url,
102+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_API_URL',
103+
description: 'The endpoint API URL to publish metrics to. (Note: you can also point to a `file://` URL to write the payload to a file instead)',
104+
type: String,
105+
optional: false
106+
),
107+
FastlaneCore::ConfigItem.new(
108+
key: :api_token,
109+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_API_TOKEN',
110+
description: 'The bearer token to call the API. Required, unless `api_url` is a `file://` URL',
111+
type: String,
112+
optional: true
113+
),
114+
FastlaneCore::ConfigItem.new(
115+
key: :use_gzip_content_encoding,
116+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_USE_GZIP_CONTENT_ENCODING',
117+
description: 'Specify that we should use `Content-Encoding: gzip` and gzip the body when sending the request',
118+
type: FastlaneCore::Boolean,
119+
default_value: true
120+
),
121+
FastlaneCore::ConfigItem.new(
122+
key: :app_name,
123+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APP_NAME',
124+
description: 'The name of the app for which we are publishing metrics, to help filter by app in the dashboard',
125+
type: String,
126+
optional: false
127+
),
128+
FastlaneCore::ConfigItem.new(
129+
key: :app_version_name,
130+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APP_VERSION_NAME',
131+
description: 'The version name of the app for which we are publishing metrics, to help filter by version in the dashboard',
132+
type: String,
133+
optional: false
134+
),
135+
FastlaneCore::ConfigItem.new(
136+
key: :app_version_code,
137+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APP_VERSION_CODE',
138+
description: 'The version code of the app for which we are publishing metrics, to help filter by version in the dashboard',
139+
type: Integer,
140+
optional: true
141+
),
142+
FastlaneCore::ConfigItem.new(
143+
key: :product_flavor,
144+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_PRODUCT_FLAVOR',
145+
description: 'The product flavor for which we are publishing metrics, to help filter by flavor in the dashboard. E.g. `Vanilla`, `Jalapeno`, `Wasabi`',
146+
type: String,
147+
optional: true
148+
),
149+
FastlaneCore::ConfigItem.new(
150+
key: :build_type,
151+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_BUILD_TYPE',
152+
description: 'The build type for which we are publishing metrics, to help filter by build type in the dashboard. E.g. `Debug`, `Release`',
153+
type: String,
154+
optional: true
155+
),
156+
FastlaneCore::ConfigItem.new(
157+
key: :source,
158+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_SOURCE',
159+
description: 'The type of event at the origin of that build, to help filter data in the dashboard. E.g. `pr`, `beta`, `final-release`',
160+
type: String,
161+
optional: true
162+
),
163+
FastlaneCore::ConfigItem.new(
164+
key: :aab_path,
165+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_AAB_PATH',
166+
description: 'The path to the .aab to extract size information from',
167+
type: String,
168+
optional: false,
169+
verify_block: proc do |value|
170+
UI.user_error!('You must provide an path to an existing `.aab` file') unless File.exist?(value)
171+
end
172+
),
173+
FastlaneCore::ConfigItem.new(
174+
key: :include_split_sizes,
175+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_INCLUDE_SPLIT_SIZES',
176+
description: 'Indicate if we should use `bundletool` and `apkanalyzer` to also compute and send "split apk" sizes per architecture. ' \
177+
+ 'Setting this to `true` adds a bit of extra time to generate the `.apk` and extract the data, but provides more detailed metrics',
178+
type: FastlaneCore::Boolean,
179+
default_value: true
180+
),
181+
FastlaneCore::ConfigItem.new(
182+
key: :apkanalyzer_binary,
183+
env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APKANALYZER_BINARY',
184+
description: 'The path to the `apkanalyzer` binary to use. If not provided explicitly, we will use `$PATH` and `$ANDROID_SDK_HOME` to try to find it',
185+
type: String,
186+
default_value: find_apkanalyzer_binary,
187+
default_value_dynamic: true,
188+
verify_block: proc do |value|
189+
UI.user_error!('You must provide a path to an existing executable for `apkanalyzer`') unless File.executable?(value)
190+
end
191+
),
192+
]
193+
end
194+
195+
def self.return_type
196+
:integer
197+
end
198+
199+
def self.return_value
200+
'The HTTP return code from the call. Expect a 201 when new metrics were received successfully and entries created in the database'
201+
end
202+
203+
def self.authors
204+
['automattic']
205+
end
206+
207+
def self.is_supported?(platform)
208+
platform == :android
209+
end
210+
end
211+
end
212+
end

0 commit comments

Comments
 (0)