From 12c74945f868f170817f7f53111fa6d1e40bd728 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Mon, 9 May 2022 18:46:25 +0200 Subject: [PATCH 01/13] Introduce AppSizeMetricsHelper To help build a payload for the grouped-metrics API --- .../helper/app_size_metrics_helper.rb | 95 +++++++++++ spec/app_size_metrics_helper_spec.rb | 153 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb create mode 100644 spec/app_size_metrics_helper_spec.rb diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb new file mode 100644 index 000000000..5e3c6decb --- /dev/null +++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb @@ -0,0 +1,95 @@ +require 'json' +require 'net/http' +require 'zlib' + +module Fastlane + module WPMRT + # A helper class to build an App Size Metrics payload and send it to a server (or write it to disk) + # + # The payload generated (and sent) by this helper conforms to the API for grouped metrics described in + # https://github.com/Automattic/apps-metrics + # + class AppSizeMetricsHelper + # @param [Hash] group_meta Metadata common to all the metrics. Can be any arbitrary set of key/value pairs. + # + def initialize(group_meta = {}) + self.meta = group_meta + @metrics = [] + end + + # Sets the metadata common to the whole group of metrics in the payload being built by this helper instance + # + # @param [Hash] hash The metadata common to all the metrics of the payload built by that helper instance. Can be any arbitrary set of key/value pairs + # + def meta=(hash) + @meta = (hash.compact || {}).map { |key, value| { name: key.to_s, value: value } } + end + + # Adds a single metric to the group of metrics + # + # @param [String] name The metric name + # @param [Integer] value The metric value + # @param [Hash] meta The arbitrary dictionary of metadata to associate to that metric entry + # + def add_metric(name:, value:, meta: nil) + metric = { name: name, value: value } + meta = (meta || {}).compact # Remove nil values if any + metric[:meta] = meta.map { |meta_key, meta_value| { name: meta_key.to_s, value: meta_value } } unless meta.empty? + @metrics.append(metric) + end + + def to_h + { + meta: @meta, + metrics: @metrics + } + end + + # Send the metrics to the given App Metrics endpoint. + # + # Must conform to the API described in https://github.com/Automattic/apps-metrics/wiki/Queue-Group-of-Metrics + # + # @param [String,URI] base_url The base URL of the App Metrics service + # @param [String] api_token The API bearer token to use to register the metric. + # @return [Integer] the HTTP response code + # + def send_metrics(base_url:, api_token:, use_gzip: true) + uri = URI(base_url) + json_payload = use_gzip ? Zlib.gzip(to_h.to_json) : to_h.to_json + + # Allow using a `file:` URI for debugging + if uri.is_a?(URI::File) + UI.message("Writing metrics payload to file #{uri.path} (instead of sending it to a server)") + File.write(uri.path, json_payload) + return 201 # To make it easy at call site to check for pseudo-status code 200 even in non-HTTP cases + end + + UI.message("Sending metrics to #{uri}...") + headers = { + Authorization: "Bearer #{api_token}", + Accept: 'application/json', + 'Content-Type': 'application/json' + } + headers[:'Content-Encoding'] = 'gzip' if use_gzip + + request = Net::HTTP::Post.new(uri, headers) + request.body = json_payload + + response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http| + http.request(request) + end + + if response.is_a?(Net::HTTPSuccess) + UI.message("Metrics sent. (#{response.code} #{response.message})") + else + UI.error("Metrics failed to send. Received: #{response.code} #{response.message}") + UI.message("Request was #{request.method} to #{request.uri}") + UI.message("Request headers were: #{headers}") + UI.message("Request body was #{request.body.length} bytes") + UI.message("Response was #{response.body}") + end + response.code.to_i + end + end + end +end diff --git a/spec/app_size_metrics_helper_spec.rb b/spec/app_size_metrics_helper_spec.rb new file mode 100644 index 000000000..b9e4cebda --- /dev/null +++ b/spec/app_size_metrics_helper_spec.rb @@ -0,0 +1,153 @@ +require_relative './spec_helper' + +describe Fastlane::WPMRT::AppSizeMetricsHelper do + describe '#to_h' do + it 'generates the right payload from raw data' do + metrics_helper = described_class.new({ + 'Group Metadata 1': 'Group Value 1', + 'Group Metadata 2': 'Group Value 2' + }) + metrics_helper.add_metric(name: 'Metric 1', value: 12_345, meta: { m1a: 'Metric 1 Metadata A' }) + metrics_helper.add_metric(name: 'Metric 2', value: 67_890) + metrics_helper.add_metric(name: 'Metric 3', value: 13_579, meta: { m3a: 'Metric 3 Metadata A', m3b: 'Metric 3 Metadata B' }) + + expected_hash = { + meta: [ + { name: 'Group Metadata 1', value: 'Group Value 1' }, + { name: 'Group Metadata 2', value: 'Group Value 2' }, + ], + metrics: [ + { name: 'Metric 1', value: 12_345, meta: [{ name: 'm1a', value: 'Metric 1 Metadata A' }] }, + { name: 'Metric 2', value: 67_890 }, + { name: 'Metric 3', value: 13_579, meta: [{ name: 'm3a', value: 'Metric 3 Metadata A' }, { name: 'm3b', value: 'Metric 3 Metadata B' }] }, + ] + } + expect(metrics_helper.to_h).to eq(expected_hash) + end + + it 'removes `nil` values in metadata' do + metrics_helper = described_class.new({ + 'Group Metadata 1': 'Group Value 1', + 'Group Metadata 2': nil, + 'Group Metadata 3': 'Group Value 3' + }) + metrics_helper.add_metric(name: 'Metric 1', value: 12_345, meta: { m1a: 'Metric 1 Metadata A', m1b: nil, m1c: 'Metric 1 Metadata C' }) + metrics_helper.add_metric(name: 'Metric 2', value: 67_890, meta: { m2a: nil }) + metrics_helper.add_metric(name: 'Metric 3', value: 13_579, meta: { m3a: 'Metric 3 Metadata A', m3b: 'Metric 3 Metadata B' }) + + expected_hash = { + meta: [ + { name: 'Group Metadata 1', value: 'Group Value 1' }, + { name: 'Group Metadata 3', value: 'Group Value 3' }, + ], + metrics: [ + { name: 'Metric 1', value: 12_345, meta: [{ name: 'm1a', value: 'Metric 1 Metadata A' }, { name: 'm1c', value: 'Metric 1 Metadata C' }] }, + { name: 'Metric 2', value: 67_890 }, + { name: 'Metric 3', value: 13_579, meta: [{ name: 'm3a', value: 'Metric 3 Metadata A' }, { name: 'm3b', value: 'Metric 3 Metadata B' }] }, + ] + } + expect(metrics_helper.to_h).to eq(expected_hash) + end + end + + describe '#send_metrics' do + let(:metrics_helper) do + metrics_helper = described_class.new({ + 'Group Metadata 1': 'Group Value 1', + 'Group Metadata 2': 'Group Value 2' + }) + metrics_helper.add_metric(name: 'Metric 1', value: 12_345, meta: { m1a: 'Metric 1 Metadata A' }) + metrics_helper.add_metric(name: 'Metric 2', value: 67_890) + metrics_helper.add_metric(name: 'Metric 3', value: 13_579, meta: { m3a: 'Metric 3 Metadata A', m3b: 'Metric 3 Metadata B' }) + metrics_helper + end + let(:expected_data) do + { + meta: [ + { name: 'Group Metadata 1', value: 'Group Value 1' }, + { name: 'Group Metadata 2', value: 'Group Value 2' }, + ], + metrics: [ + { name: 'Metric 1', value: 12_345, meta: [{ name: 'm1a', value: 'Metric 1 Metadata A' }] }, + { name: 'Metric 2', value: 67_890 }, + { name: 'Metric 3', value: 13_579, meta: [{ name: 'm3a', value: 'Metric 3 Metadata A' }, { name: 'm3b', value: 'Metric 3 Metadata B' }] }, + ] + }.to_json + end + + context 'when using file:// scheme for the URL' do + it 'writes the payload uncompressed to a file when disabling gzip' do + in_tmp_dir do |tmp_dir| + output_file = File.join(tmp_dir, 'payload.json') + base_url = File.join('file://localhost/', output_file) + + code = metrics_helper.send_metrics(base_url: base_url, api_token: nil, use_gzip: false) + + expect(code).to eq(201) + expect(File).to exist(output_file) + uncompressed_data = File.read(output_file) + expect(uncompressed_data).to eq(expected_data) + end + end + + it 'writes the payload compressed to a file when enabling gzip' do + in_tmp_dir do |tmp_dir| + output_file = File.join(tmp_dir, 'payload.json.gz') + base_url = File.join('file://localhost/', output_file) + + code = metrics_helper.send_metrics(base_url: base_url, api_token: nil, use_gzip: true) + + expect(code).to eq(201) + expect(File).to exist(output_file) + uncompressed_data = Zlib::GzipReader.open(output_file, &:read) + expect(uncompressed_data).to eq(expected_data) + end + end + end + + context 'when using non-file:// scheme for the URL' do + let(:base_url) { 'https://fake-metrics-server/api/grouped-metrics' } + let(:token) { 'fake#tokn' } + + it 'sends the payload uncompressed to the server and with the right headers when disabling gzip' do + expected_headers = { + Authorization: "Bearer #{token}", + Accept: 'application/json', + 'Content-Type': 'application/json' + } + last_received_body = nil + stub = stub_request(:post, base_url).with(headers: expected_headers) do |req| + last_received_body = req.body + end.to_return(status: 201) + + code = metrics_helper.send_metrics(base_url: base_url, api_token: token, use_gzip: false) + + expect(code).to eq(201) + expect(stub).to have_been_made.once + expect(last_received_body).to eq(expected_data) + end + + it 'sends the payload compressed to the server and with the right headers when enabling gzip' do + expected_headers = { + Authorization: "Bearer #{token}", + Accept: 'application/json', + 'Content-Type': 'application/json', + 'Content-Encoding': 'gzip' + } + last_received_body = nil + stub = stub_request(:post, base_url).with(headers: expected_headers) do |req| + last_received_body = req.body + end.to_return(status: 201) + + code = metrics_helper.send_metrics(base_url: base_url, api_token: token, use_gzip: true) + + expect(code).to eq(201) + expect(stub).to have_been_made.once + expect do + last_received_body = Zlib.gunzip(last_received_body) + end.not_to raise_error + expect(last_received_body).to eq(expected_data) + end + end + end +end From 62ecdf70a3416b97536051a4f631ad4f83ffb94e Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 17 May 2022 17:51:30 +0200 Subject: [PATCH 02/13] ios_send_app_size_metrics action + specs --- Gemfile.lock | 3 +- fastlane-plugin-wpmreleasetoolkit.gemspec | 1 + .../actions/ios/ios_send_app_size_metrics.rb | 165 ++ spec/ios_send_app_size_metrics_spec.rb | 102 + .../app_size_metrics/app-thinning.plist | 1686 +++++++++++++++++ .../app_size_metrics/ios-metrics-payload.json | 434 +++++ 6 files changed, 2390 insertions(+), 1 deletion(-) create mode 100644 lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb create mode 100644 spec/ios_send_app_size_metrics_spec.rb create mode 100644 spec/test-data/app_size_metrics/app-thinning.plist create mode 100644 spec/test-data/app_size_metrics/ios-metrics-payload.json diff --git a/Gemfile.lock b/Gemfile.lock index 47a064c8a..d66e63921 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,6 +12,7 @@ PATH nokogiri (~> 1.11) octokit (~> 4.18) parallel (~> 1.14) + plist (~> 3.1) progress_bar (~> 1.3) rake (>= 12.3, < 14.0) rake-compiler (~> 1.0) @@ -427,4 +428,4 @@ DEPENDENCIES yard BUNDLED WITH - 2.2.33 + 2.3.13 diff --git a/fastlane-plugin-wpmreleasetoolkit.gemspec b/fastlane-plugin-wpmreleasetoolkit.gemspec index da2858439..598bd3ceb 100644 --- a/fastlane-plugin-wpmreleasetoolkit.gemspec +++ b/fastlane-plugin-wpmreleasetoolkit.gemspec @@ -29,6 +29,7 @@ Gem::Specification.new do |spec| spec.add_dependency 'nokogiri', '~> 1.11' # Needed for AndroidLocalizeHelper spec.add_dependency 'octokit', '~> 4.18' spec.add_dependency 'buildkit', '~> 1.5' + spec.add_dependency 'plist', '~> 3.1' spec.add_dependency 'git', '~> 1.3' spec.add_dependency 'jsonlint', '~> 0.3' spec.add_dependency 'rake', '>= 12.3', '< 14.0' diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb new file mode 100644 index 000000000..338255b84 --- /dev/null +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb @@ -0,0 +1,165 @@ +require 'plist' +require_relative '../../helper/app_size_metrics_helper' + +module Fastlane + module Actions + class IosSendAppSizeMetricsAction < Action + def self.run(params) + # Check input parameters + base_url = URI(params[:api_base_url]) + api_token = params[:api_token] + if (api_token.nil? || api_token.empty?) && !base_url.is_a?(URI::File) + UI.user_error!('An API token is required when using an `api_base_url` with a scheme other than `file://`') + end + + # Build the payload base + metrics_helper = Fastlane::WPMRT::AppSizeMetricsHelper.new( + Platform: 'iOS', + 'App Name': params[:app_name], + 'App Version': params[:app_version], + 'Build Type': params[:build_type], + Source: params[:source] + ) + metrics_helper.add_metric(name: 'File Size', value: File.size(params[:ipa_path])) + + # Add app-thinning metrics to the payload if a `.plist` is provided + app_thinning_plist_path = params[:app_thinning_plist_path] || File.join(File.dirname(params[:ipa_path]), 'app-thinning.plist') + if File.exist?(app_thinning_plist_path) + plist = Plist.parse_xml(app_thinning_plist_path) + plist['variants'].each do |_key, variant| + variant_descriptors = variant['variantDescriptors'] || [{ 'device' => 'Universal' }] + variant_descriptors.each do |desc| + variant_metadata = { device: desc['device'], 'OS Version': desc['os-version'] } + metrics_helper.add_metric(name: 'Download Size', value: variant['sizeCompressedApp'], meta: variant_metadata) + metrics_helper.add_metric(name: 'Install Size', value: variant['sizeUncompressedApp'], meta: variant_metadata) + end + end + end + + # Send the payload + metrics_helper.send_metrics( + base_url: base_url, + api_token: api_token, + use_gzip: params[:use_gzip_content_encoding] + ) + end + + ##################################################### + # @!group Documentation + ##################################################### + + def self.description + 'Send iOS app size metrics to our metrics server' + end + + def self.details + <<~DETAILS + Send iOS app size metrics to our metrics server. + + In order to get Xcode generate the `app-thinning.plist` file (during `gym` and the export of the `.xcarchive`), you need to: + (1) Use either `ad-hoc`, `enterprise` or `development` export method (in particular, won't work with `app-store`), + (2) Provide `thinning: ''` as part of your `export_options` of `gym` (or in your `options.plist` file if you use raw `xcodebuild`) + See https://help.apple.com/xcode/mac/11.0/index.html#/devde46df08a + + For builds exported with the `app-store` method, `xcodebuild` won't generate an `app-thinning.plist` file; so you will only be able to get + the Universal `.ipa` file size as a metric, but won't get the per-device, broken-down install and download sizes for each thinned variant. + + See https://github.com/Automattic/apps-metrics for the API contract expected by the Metrics server you are expected to send those metrics to. + + Tip: If you provide a `file://` URL for the `api_base_url`, the action will write the payload on disk at the specified path instead of sending + 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. + DETAILS + end + + def self.available_options + [ + FastlaneCore::ConfigItem.new( + key: :api_base_url, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_API_BASE_URL', + 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)', + type: String, + optional: false + ), + FastlaneCore::ConfigItem.new( + key: :api_token, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_API_TOKEN', + description: 'The bearer token to call the API. Required, unless `api_base_url` is a `file://` URL', + type: String, + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :use_gzip_content_encoding, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_USE_GZIP_CONTENT_ENCODING', + description: 'Specify that we should use `Content-Encoding: gzip` and gzip the body when sending the request', + type: FastlaneCore::Boolean, + default_value: true + ), + FastlaneCore::ConfigItem.new( + key: :app_name, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_APP_NAME', + description: 'The name of the app for which we are publishing metrics, to help filter by app in the dashboard', + type: String, + optional: false + ), + FastlaneCore::ConfigItem.new( + key: :app_version, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_APP_VERSION', + description: 'The version of the app for which we are publishing metrics, to help filter by version in the dashboard', + type: String, + optional: false + ), + FastlaneCore::ConfigItem.new( + key: :build_type, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_BUILD_TYPE', + description: 'The build configuration for which we are publishing metrics, to help filter by build config in the dashboard. E.g. `Debug`, `Release`', + type: String, + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :source, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_SOURCE', + description: 'The type of event at the origin of that build, to help filter data in the dashboard. E.g. `pr`, `beta`, `final-release`', + type: String, + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :ipa_path, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_IPA_PATH', + description: 'The path to the .ipa to extract size information from', + type: String, + optional: false, + default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH], + verify_block: proc do |value| + UI.user_error!('You must provide an path to an existing `.ipa` file') unless File.exist?(value) + end + ), + FastlaneCore::ConfigItem.new( + key: :app_thinning_plist_path, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_APP_THINNING_PLIST_PATH', + description: 'The path to the `app-thinning.plist` file to extract thinning size information from. ' \ + + 'By default, will try to use the `app-thinning.plist` file next to the `ipa_path`, if that file exists', + type: String, + optional: true, + default_value_dynamic: true + ), + ] + end + + def self.return_type + :integer + end + + def self.return_value + 'The HTTP return code from the call. Expect a 201 when new metrics were received successfully and entries created in the database' + end + + def self.authors + ['automattic'] + end + + def self.is_supported?(platform) + [:ios, :mac].include? platform + end + end + end +end diff --git a/spec/ios_send_app_size_metrics_spec.rb b/spec/ios_send_app_size_metrics_spec.rb new file mode 100644 index 000000000..95ec5ce4a --- /dev/null +++ b/spec/ios_send_app_size_metrics_spec.rb @@ -0,0 +1,102 @@ +require_relative './spec_helper' + +describe Fastlane::Actions::IosSendAppSizeMetricsAction do + let(:test_data_dir) { File.join(File.dirname(__FILE__), 'test-data', 'app_size_metrics') } + let(:fake_ipa_size) { 1337 } # The value used in the `app-thinning.plist` and `ios-metrics-payload.json` fixtures + + def test_app_size_action(fake_ipa_size:, expected_payload:, **other_action_args) + in_tmp_dir do |tmp_dir| + # Arrange + output_file = File.join(tmp_dir, 'output-payload') + ipa_path = File.join(tmp_dir, 'fake.ipa') + File.write(ipa_path, '-fake-ipa-file-') + allow(File).to receive(:size).with(ipa_path).and_return(fake_ipa_size) + + # Act + code = run_described_fastlane_action( + api_base_url: File.join('file://localhost/', output_file), + ipa_path: ipa_path, + **other_action_args + ) + + # Asserts + expect(code).to eq(201) + expect(File).to exist(output_file) + gzip_disabled = other_action_args[:use_gzip_content_encoding] == false + generated_payload = gzip_disabled ? File.read(output_file) : Zlib::GzipReader.open(output_file, &:read) + # Compare the payloads as pretty-formatted JSON, to make the diff in test failures more readable if one happen + expect(JSON.pretty_generate(JSON.parse(generated_payload))).to eq(JSON.pretty_generate(expected_payload)), 'Decompressed JSON payload was not as expected' + # Compare the payloads as raw uncompressed data as a final check + expect(generated_payload).to eq(expected_payload.to_json) + end + end + + context 'when only providing an `.ipa` file with no `app-thinning.plist` file' do + it 'generates the expected payload, compressed by default' do + expected = { + meta: [ + { name: 'Platform', value: 'iOS' }, + { name: 'App Name', value: 'my-app' }, + { name: 'App Version', value: '1.2.3' }, + { name: 'Build Type', value: 'beta' }, + { name: 'Source', value: 'unit-test' }, + ], + metrics: [ + { name: 'File Size', value: 123_456 }, + ] + } + + test_app_size_action( + fake_ipa_size: 123_456, + expected_payload: expected, + app_name: 'my-app', + build_type: 'beta', + app_version: '1.2.3', + source: 'unit-test' + ) + end + + it 'generates the expected payload, uncompressed when disabling gzip' do + expected = { + meta: [ + { name: 'Platform', value: 'iOS' }, + { name: 'App Name', value: 'my-app' }, + { name: 'App Version', value: '1.2.3' }, + { name: 'Build Type', value: 'beta' }, + { name: 'Source', value: 'unit-test' }, + ], + metrics: [ + { name: 'File Size', value: 123_456 }, + ] + } + + test_app_size_action( + fake_ipa_size: 123_456, + expected_payload: expected, + app_name: 'my-app', + build_type: 'beta', + app_version: '1.2.3', + source: 'unit-test', + use_gzip_content_encoding: false + ) + end + end + + context 'when using both an `.ipa` file and an existing `app-thinning.plist` file' do + it 'generates the expected payload containing both the Universal and optimized thinned sizes' do + app_thinning_plist_path = File.join(test_data_dir, 'app-thinning.plist') + expected_fixture = File.join(test_data_dir, 'ios-metrics-payload.json') + expected = JSON.parse(File.read(expected_fixture)) + + test_app_size_action( + fake_ipa_size: fake_ipa_size, + expected_payload: expected, + app_thinning_plist_path: app_thinning_plist_path, + app_name: 'wordpress', + build_type: 'internal', + app_version: '19.8.0.2', + source: 'unit-test' + ) + end + end +end diff --git a/spec/test-data/app_size_metrics/app-thinning.plist b/spec/test-data/app_size_metrics/app-thinning.plist new file mode 100644 index 000000000..a3ba80e73 --- /dev/null +++ b/spec/test-data/app_size_metrics/app-thinning.plist @@ -0,0 +1,1686 @@ + + + + + variants + + Apps/WordPress-0F68C443-4038-4AED-BDD3-9EA10588AC99.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73174415 + sizeCompressedAppAndODR + 73174415 + sizeCompressedODR + 0 + sizeUncompressedApp + 164397537 + sizeUncompressedAppAndODR + 164397537 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPhone11,8 + os-version + 15.0 + + + device + iPhone12,1 + os-version + 15.0 + + + + Apps/WordPress-328AA1BD-36C1-474F-8974-30E9CA61BAAF.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 80810992 + sizeCompressedAppAndODR + 80810992 + sizeCompressedODR + 0 + sizeUncompressedApp + 172226929 + sizeUncompressedAppAndODR + 172226929 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPhone14,5 + os-version + 15.0 + + + device + iPhone13,2 + os-version + 15.0 + + + device + iPhone13,3 + os-version + 15.0 + + + device + iPhone13,1 + os-version + 15.0 + + + device + iPhone13,4 + os-version + 15.0 + + + device + iPhone14,2 + os-version + 15.0 + + + device + iPhone12,3 + os-version + 15.0 + + + device + iPhone10,3 + os-version + 15.0 + + + device + iPhone11,4 + os-version + 15.0 + + + device + iPhone14,4 + os-version + 15.0 + + + device + iPhone11,6 + os-version + 15.0 + + + device + iPhone12,5 + os-version + 15.0 + + + device + iPhone10,2 + os-version + 15.0 + + + device + iPhone10,5 + os-version + 15.0 + + + device + iPhone14,3 + os-version + 15.0 + + + device + iPhone11,2 + os-version + 15.0 + + + device + iPhone10,6 + os-version + 15.0 + + + device + iPhone9,2 + os-version + 15.0 + + + device + iPhone9,4 + os-version + 15.0 + + + + Apps/WordPress-3794860D-098D-4F74-B660-6FF02FDD13E0.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73223402 + sizeCompressedAppAndODR + 73223402 + sizeCompressedODR + 0 + sizeUncompressedApp + 164449045 + sizeUncompressedAppAndODR + 164449045 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPad5,1 + os-version + 15.0 + + + device + iPad7,11 + os-version + 15.0 + + + device + iPad6,12 + os-version + 15.0 + + + device + iPad5,4 + os-version + 15.0 + + + device + iPad6,8 + os-version + 15.0 + + + device + iPad6,11 + os-version + 15.0 + + + device + iPad7,5 + os-version + 15.0 + + + device + iPad7,6 + os-version + 15.0 + + + device + iPad12,2 + os-version + 15.0 + + + device + iPad7,12 + os-version + 15.0 + + + device + iPad5,3 + os-version + 15.0 + + + device + iPad11,6 + os-version + 15.0 + + + device + iPad12,1 + os-version + 15.0 + + + device + iPad6,7 + os-version + 15.0 + + + device + iPad11,7 + os-version + 15.0 + + + device + iPad5,2 + os-version + 15.0 + + + + Apps/WordPress-37DB2251-6089-4171-9B36-60F44E24D78C.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73165631 + sizeCompressedAppAndODR + 73165631 + sizeCompressedODR + 0 + sizeUncompressedApp + 164387854 + sizeUncompressedAppAndODR + 164387854 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPod9,1 + os-version + 15.0 + + + device + iPhone8,4 + os-version + 15.0 + + + device + iPhone8,1 + os-version + 15.0 + + + + Apps/WordPress-3B92F02C-3F0A-4F4F-BA8A-30E54AE89DCD.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73346331 + sizeCompressedAppAndODR + 73346331 + sizeCompressedODR + 0 + sizeUncompressedApp + 164840713 + sizeUncompressedAppAndODR + 164840713 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPad7,5 + os-version + 14.0 + + + device + iPad7,11 + os-version + 13.0 + + + device + iPad5,2 + os-version + 13.0 + + + device + iPad7,12 + os-version + 13.0 + + + device + iPad6,8 + os-version + 13.0 + + + device + iPad7,6 + os-version + 14.0 + + + device + iPad5,1 + os-version + 13.0 + + + device + iPad5,4 + os-version + 14.0 + + + device + iPad6,11 + os-version + 14.0 + + + device + iPad7,5 + os-version + 13.0 + + + device + iPad6,8 + os-version + 14.0 + + + device + iPad11,7 + os-version + 14.0 + + + device + iPad5,3 + os-version + 14.0 + + + device + iPad5,1 + os-version + 14.0 + + + device + iPad7,6 + os-version + 13.0 + + + device + iPad7,11 + os-version + 14.0 + + + device + iPad6,7 + os-version + 14.0 + + + device + iPad7,12 + os-version + 14.0 + + + device + iPad6,11 + os-version + 13.0 + + + device + iPad6,12 + os-version + 14.0 + + + device + iPad6,7 + os-version + 13.0 + + + device + iPad5,3 + os-version + 13.0 + + + device + iPad11,6 + os-version + 14.0 + + + device + iPad6,12 + os-version + 13.0 + + + device + iPad5,4 + os-version + 13.0 + + + device + iPad5,2 + os-version + 14.0 + + + + Apps/WordPress-47153C09-1B8C-4F22-910F-ED9BAAA9C32F.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73107750 + sizeCompressedAppAndODR + 73107750 + sizeCompressedODR + 0 + sizeUncompressedApp + 164330181 + sizeUncompressedAppAndODR + 164330181 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPhone12,8 + os-version + 15.0 + + + device + iPhone9,3 + os-version + 15.0 + + + device + iPhone10,1 + os-version + 15.0 + + + device + iPhone10,4 + os-version + 15.0 + + + device + iPhone9,1 + os-version + 15.0 + + + + Apps/WordPress-70B6F7AE-03BC-4155-9C0E-50A66EF09788.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73165642 + sizeCompressedAppAndODR + 73165642 + sizeCompressedODR + 0 + sizeUncompressedApp + 164391941 + sizeUncompressedAppAndODR + 164391941 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPad13,1 + os-version + 15.0 + + + device + iPad14,2 + os-version + 15.0 + + + device + iPad13,2 + os-version + 15.0 + + + device + iPad8,6 + os-version + 15.0 + + + device + iPad13,7 + os-version + 15.0 + + + device + iPad8,11 + os-version + 15.0 + + + device + iPad8,1 + os-version + 15.0 + + + device + iPad8,4 + os-version + 15.0 + + + device + iPad13,11 + os-version + 15.0 + + + device + iPad13,10 + os-version + 15.0 + + + device + iPad11,1 + os-version + 15.0 + + + device + iPad11,3 + os-version + 15.0 + + + device + iPad13,6 + os-version + 15.0 + + + device + iPad8,3 + os-version + 15.0 + + + device + iPad7,1 + os-version + 15.0 + + + device + iPad8,7 + os-version + 15.0 + + + device + iPad13,5 + os-version + 15.0 + + + device + iPad11,2 + os-version + 15.0 + + + device + iPad7,4 + os-version + 15.0 + + + device + iPad8,8 + os-version + 15.0 + + + device + iPad7,2 + os-version + 15.0 + + + device + iPad14,1 + os-version + 15.0 + + + device + iPad6,4 + os-version + 15.0 + + + device + iPad8,10 + os-version + 15.0 + + + device + iPad8,9 + os-version + 15.0 + + + device + iPad13,4 + os-version + 15.0 + + + device + iPad7,3 + os-version + 15.0 + + + device + iPad11,4 + os-version + 15.0 + + + device + iPad8,12 + os-version + 15.0 + + + device + iPad13,9 + os-version + 15.0 + + + device + iPad6,3 + os-version + 15.0 + + + device + iPad8,2 + os-version + 15.0 + + + device + MacFamily20,1 + os-version + 15.0 + + + device + iPad8,5 + os-version + 15.0 + + + device + iPad13,8 + os-version + 15.0 + + + + Apps/WordPress-95371D74-4783-480D-8A7E-EE15661E5B8D.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73288679 + sizeCompressedAppAndODR + 73288679 + sizeCompressedODR + 0 + sizeUncompressedApp + 164779586 + sizeUncompressedAppAndODR + 164779586 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPod9,1 + os-version + 14.0 + + + device + iPhone8,4 + os-version + 14.0 + + + device + iPhone8,1 + os-version + 13.0 + + + device + iPhone8,4 + os-version + 13.0 + + + device + iPod9,1 + os-version + 13.0 + + + device + iPhone8,1 + os-version + 14.0 + + + + Apps/WordPress-9CD32B95-A5D1-4274-839C-640FDAD77B26.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73230799 + sizeCompressedAppAndODR + 73230799 + sizeCompressedODR + 0 + sizeUncompressedApp + 164721913 + sizeUncompressedAppAndODR + 164721913 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPhone9,3 + os-version + 13.0 + + + device + iPhone9,3 + os-version + 14.0 + + + device + iPhone10,1 + os-version + 14.0 + + + device + iPhone10,1 + os-version + 13.0 + + + device + iPhone9,1 + os-version + 14.0 + + + device + iPhone10,4 + os-version + 14.0 + + + device + iPhone12,8 + os-version + 14.0 + + + device + iPhone10,4 + os-version + 13.0 + + + device + iPhone12,8 + os-version + 13.0 + + + device + iPhone9,1 + os-version + 13.0 + + + + Apps/WordPress-B1AFCDA3-9593-42C4-ACE5-FD91583FD214.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73288723 + sizeCompressedAppAndODR + 73288723 + sizeCompressedODR + 0 + sizeUncompressedApp + 164783609 + sizeUncompressedAppAndODR + 164783609 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPad6,4 + os-version + 14.0 + + + device + iPad6,3 + os-version + 13.0 + + + device + iPad7,2 + os-version + 14.0 + + + device + iPad8,1 + os-version + 13.0 + + + device + iPad13,7 + os-version + 14.0 + + + device + iPad8,11 + os-version + 14.0 + + + device + iPad8,6 + os-version + 13.0 + + + device + iPad13,11 + os-version + 14.0 + + + device + iPad8,1 + os-version + 14.0 + + + device + iPad13,4 + os-version + 14.0 + + + device + iPad8,10 + os-version + 13.0 + + + device + iPad7,3 + os-version + 14.0 + + + device + iPad13,8 + os-version + 14.0 + + + device + iPad11,4 + os-version + 14.0 + + + device + iPad6,4 + os-version + 13.0 + + + device + iPad8,7 + os-version + 14.0 + + + device + iPad8,4 + os-version + 13.0 + + + device + iPad7,1 + os-version + 14.0 + + + device + iPad8,4 + os-version + 14.0 + + + device + iPad7,2 + os-version + 13.0 + + + device + iPad13,1 + os-version + 14.0 + + + device + iPad11,2 + os-version + 14.0 + + + device + iPad8,12 + os-version + 13.0 + + + device + iPad8,6 + os-version + 14.0 + + + device + iPad8,8 + os-version + 13.0 + + + device + iPad8,2 + os-version + 14.0 + + + device + iPad8,11 + os-version + 13.0 + + + device + iPad13,10 + os-version + 14.0 + + + device + iPad11,3 + os-version + 13.0 + + + device + iPad8,8 + os-version + 14.0 + + + device + iPad11,2 + os-version + 13.0 + + + device + iPad8,12 + os-version + 14.0 + + + device + iPad13,9 + os-version + 14.0 + + + device + iPad7,3 + os-version + 13.0 + + + device + iPad13,2 + os-version + 14.0 + + + device + iPad13,5 + os-version + 14.0 + + + device + MacFamily20,1 + os-version + 14.0 + + + device + iPad11,1 + os-version + 14.0 + + + device + iPad8,2 + os-version + 13.0 + + + device + iPad6,3 + os-version + 14.0 + + + device + iPad7,4 + os-version + 13.0 + + + device + iPad13,6 + os-version + 14.0 + + + device + iPad8,9 + os-version + 13.0 + + + device + iPad8,3 + os-version + 14.0 + + + device + iPad8,7 + os-version + 13.0 + + + device + iPad8,5 + os-version + 13.0 + + + device + iPad11,1 + os-version + 13.0 + + + device + iPad8,5 + os-version + 14.0 + + + device + iPad11,3 + os-version + 14.0 + + + device + iPad8,9 + os-version + 14.0 + + + device + iPad7,4 + os-version + 14.0 + + + device + iPad7,1 + os-version + 13.0 + + + device + iPad8,10 + os-version + 14.0 + + + device + iPad11,4 + os-version + 13.0 + + + device + iPad8,3 + os-version + 13.0 + + + + Apps/WordPress-D6E80112-8444-447A-AAC1-810118FA1875.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 80933891 + sizeCompressedAppAndODR + 80933891 + sizeCompressedODR + 0 + sizeUncompressedApp + 172618525 + sizeUncompressedAppAndODR + 172618525 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPhone12,5 + os-version + 13.0 + + + device + iPhone10,6 + os-version + 14.0 + + + device + iPhone12,3 + os-version + 14.0 + + + device + iPhone10,3 + os-version + 14.0 + + + device + iPhone13,3 + os-version + 14.0 + + + device + iPhone10,5 + os-version + 14.0 + + + device + iPhone11,6 + os-version + 13.0 + + + device + iPhone11,2 + os-version + 13.0 + + + device + iPhone13,4 + os-version + 14.0 + + + device + iPhone9,4 + os-version + 13.0 + + + device + iPhone10,2 + os-version + 14.0 + + + device + iPhone11,4 + os-version + 13.0 + + + device + iPhone10,3 + os-version + 13.0 + + + device + iPhone12,5 + os-version + 14.0 + + + device + iPhone10,2 + os-version + 13.0 + + + device + iPhone10,6 + os-version + 13.0 + + + device + iPhone10,5 + os-version + 13.0 + + + device + iPhone11,4 + os-version + 14.0 + + + device + iPhone11,6 + os-version + 14.0 + + + device + iPhone9,4 + os-version + 14.0 + + + device + iPhone11,2 + os-version + 14.0 + + + device + iPhone9,2 + os-version + 14.0 + + + device + iPhone9,2 + os-version + 13.0 + + + device + iPhone12,3 + os-version + 13.0 + + + device + iPhone13,2 + os-version + 14.0 + + + device + iPhone13,1 + os-version + 14.0 + + + + Apps/WordPress-E8A051CA-A279-4725-B18B-763A1E4E1FB7.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 81097487 + sizeCompressedAppAndODR + 81097487 + sizeCompressedODR + 0 + sizeUncompressedApp + 172781682 + sizeUncompressedAppAndODR + 172781682 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPhone8,2 + os-version + 13.0 + + + device + iPhone8,2 + os-version + 14.0 + + + + Apps/WordPress-FB103AD6-CD63-43CE-B47B-19C29CB669BA.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 73297518 + sizeCompressedAppAndODR + 73297518 + sizeCompressedODR + 0 + sizeUncompressedApp + 164789269 + sizeUncompressedAppAndODR + 164789269 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPhone12,1 + os-version + 13.0 + + + device + iPhone12,1 + os-version + 14.0 + + + device + iPhone11,8 + os-version + 14.0 + + + device + iPhone11,8 + os-version + 13.0 + + + + Apps/WordPress-FD1D21DD-4330-4020-A153-8CE54A9908D2.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 80974447 + sizeCompressedAppAndODR + 80974447 + sizeCompressedODR + 0 + sizeUncompressedApp + 172389950 + sizeUncompressedAppAndODR + 172389950 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + variantDescriptors + + + device + iPhone8,2 + os-version + 15.0 + + + + Apps/WordPress.ipa + + limitUncompressedInitialPrefetchedODR + 2147483648 + onDemandResourcesAssetPacks + + sizeCompressedApp + 98758624 + sizeCompressedAppAndODR + 98758624 + sizeCompressedODR + 0 + sizeUncompressedApp + 195563137 + sizeUncompressedAppAndODR + 195563137 + sizeUncompressedInitialPrefetchedODR + 0 + sizeUncompressedInitialPrefetchedODRExceedsLimit + + sizeUncompressedODR + 0 + tagsUncompressedInitialPrefetchedODR + + + + + diff --git a/spec/test-data/app_size_metrics/ios-metrics-payload.json b/spec/test-data/app_size_metrics/ios-metrics-payload.json new file mode 100644 index 000000000..7a0b46366 --- /dev/null +++ b/spec/test-data/app_size_metrics/ios-metrics-payload.json @@ -0,0 +1,434 @@ +{ + "meta": [ + { "name": "Platform", "value": "iOS" }, + { "name": "App Name", "value": "wordpress" }, + { "name": "App Version", "value": "19.8.0.2" }, + { "name": "Build Type", "value": "internal" }, + { "name": "Source", "value": "unit-test" } + ], + "metrics": [ + { "name": "File Size", "value": 1337 }, + { "name": "Download Size", "value": 73174415, "meta": [{ "name": "device", "value": "iPhone11,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164397537, "meta": [{ "name": "device", "value": "iPhone11,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73174415, "meta": [{ "name": "device", "value": "iPhone12,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164397537, "meta": [{ "name": "device", "value": "iPhone12,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone14,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone14,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone13,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone13,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone13,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone13,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone13,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone13,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone13,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone13,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone14,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone14,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone12,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone12,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone10,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone10,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone11,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone11,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone14,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone14,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone11,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone11,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone12,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone12,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone10,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone10,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone10,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone10,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone14,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone14,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone11,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone11,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone10,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone10,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone9,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone9,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 80810992, "meta": [{ "name": "device", "value": "iPhone9,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172226929, "meta": [{ "name": "device", "value": "iPhone9,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad5,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad5,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad7,11" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad7,11" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad6,12" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad6,12" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad5,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad5,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad6,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad6,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad6,11" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad6,11" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad7,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad7,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad7,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad7,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad12,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad12,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad7,12" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad7,12" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad5,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad5,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad11,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad11,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad12,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad12,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad6,7" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad6,7" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad11,7" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad11,7" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73223402, "meta": [{ "name": "device", "value": "iPad5,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164449045, "meta": [{ "name": "device", "value": "iPad5,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165631, "meta": [{ "name": "device", "value": "iPod9,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164387854, "meta": [{ "name": "device", "value": "iPod9,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165631, "meta": [{ "name": "device", "value": "iPhone8,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164387854, "meta": [{ "name": "device", "value": "iPhone8,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165631, "meta": [{ "name": "device", "value": "iPhone8,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164387854, "meta": [{ "name": "device", "value": "iPhone8,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad7,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad7,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad7,11" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad7,11" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad5,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad5,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad7,12" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad7,12" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad6,8" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad6,8" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad7,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad7,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad5,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad5,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad5,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad5,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad6,11" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad6,11" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad7,5" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad7,5" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad6,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad6,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad11,7" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad11,7" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad5,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad5,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad5,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad5,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad7,6" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad7,6" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad7,11" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad7,11" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad6,7" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad6,7" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad7,12" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad7,12" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad6,11" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad6,11" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad6,12" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad6,12" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad6,7" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad6,7" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad5,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad5,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad11,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad11,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad6,12" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad6,12" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad5,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad5,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73346331, "meta": [{ "name": "device", "value": "iPad5,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164840713, "meta": [{ "name": "device", "value": "iPad5,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73107750, "meta": [{ "name": "device", "value": "iPhone12,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164330181, "meta": [{ "name": "device", "value": "iPhone12,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73107750, "meta": [{ "name": "device", "value": "iPhone9,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164330181, "meta": [{ "name": "device", "value": "iPhone9,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73107750, "meta": [{ "name": "device", "value": "iPhone10,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164330181, "meta": [{ "name": "device", "value": "iPhone10,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73107750, "meta": [{ "name": "device", "value": "iPhone10,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164330181, "meta": [{ "name": "device", "value": "iPhone10,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73107750, "meta": [{ "name": "device", "value": "iPhone9,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164330181, "meta": [{ "name": "device", "value": "iPhone9,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad14,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad14,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,7" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,7" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,11" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,11" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,11" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,11" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,10" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,10" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad11,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad11,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad11,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad11,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,6" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad7,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad7,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,7" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,7" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad11,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad11,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad7,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad7,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad7,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad7,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad14,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad14,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad6,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad6,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,10" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,10" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,9" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,9" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad7,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad7,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad11,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad11,4" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,12" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,12" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,9" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,9" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad6,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad6,3" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "MacFamily20,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "MacFamily20,1" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad8,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad8,5" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73165642, "meta": [{ "name": "device", "value": "iPad13,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 164391941, "meta": [{ "name": "device", "value": "iPad13,8" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 73288679, "meta": [{ "name": "device", "value": "iPod9,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164779586, "meta": [{ "name": "device", "value": "iPod9,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288679, "meta": [{ "name": "device", "value": "iPhone8,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164779586, "meta": [{ "name": "device", "value": "iPhone8,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288679, "meta": [{ "name": "device", "value": "iPhone8,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164779586, "meta": [{ "name": "device", "value": "iPhone8,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288679, "meta": [{ "name": "device", "value": "iPhone8,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164779586, "meta": [{ "name": "device", "value": "iPhone8,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288679, "meta": [{ "name": "device", "value": "iPod9,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164779586, "meta": [{ "name": "device", "value": "iPod9,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288679, "meta": [{ "name": "device", "value": "iPhone8,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164779586, "meta": [{ "name": "device", "value": "iPhone8,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone9,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone9,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone9,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone9,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone10,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone10,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone10,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone10,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone9,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone9,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone10,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone10,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone12,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone12,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone10,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone10,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone12,8" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone12,8" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73230799, "meta": [{ "name": "device", "value": "iPhone9,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164721913, "meta": [{ "name": "device", "value": "iPhone9,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad6,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad6,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad6,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad6,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad7,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad7,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,7" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,7" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,11" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,11" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,6" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,6" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,11" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,11" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,10" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,10" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad7,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad7,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad11,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad11,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad6,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad6,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,7" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,7" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad7,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad7,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad7,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad7,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad11,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad11,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,12" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,12" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,8" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,8" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,11" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,11" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,10" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,10" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad11,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad11,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad11,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad11,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,12" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,12" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,9" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,9" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad7,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad7,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "MacFamily20,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "MacFamily20,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad11,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad11,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad6,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad6,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad7,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad7,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad13,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad13,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,9" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,9" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,7" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,7" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,5" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,5" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad11,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad11,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad11,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad11,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,9" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,9" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad7,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad7,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad7,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad7,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,10" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,10" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad11,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad11,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73288723, "meta": [{ "name": "device", "value": "iPad8,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164783609, "meta": [{ "name": "device", "value": "iPad8,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone12,5" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone12,5" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone10,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone10,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone12,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone12,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone10,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone10,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone13,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone13,3" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone10,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone10,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone11,6" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone11,6" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone11,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone11,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone13,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone13,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone9,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone9,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone10,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone10,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone11,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone11,4" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone10,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone10,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone12,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone12,5" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone10,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone10,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone10,6" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone10,6" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone10,5" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone10,5" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone11,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone11,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone11,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone11,6" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone9,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone9,4" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone11,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone11,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone9,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone9,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone9,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone9,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone12,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone12,3" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone13,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone13,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 80933891, "meta": [{ "name": "device", "value": "iPhone13,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172618525, "meta": [{ "name": "device", "value": "iPhone13,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 81097487, "meta": [{ "name": "device", "value": "iPhone8,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 172781682, "meta": [{ "name": "device", "value": "iPhone8,2" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 81097487, "meta": [{ "name": "device", "value": "iPhone8,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 172781682, "meta": [{ "name": "device", "value": "iPhone8,2" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73297518, "meta": [{ "name": "device", "value": "iPhone12,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164789269, "meta": [{ "name": "device", "value": "iPhone12,1" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 73297518, "meta": [{ "name": "device", "value": "iPhone12,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164789269, "meta": [{ "name": "device", "value": "iPhone12,1" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73297518, "meta": [{ "name": "device", "value": "iPhone11,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Install Size", "value": 164789269, "meta": [{ "name": "device", "value": "iPhone11,8" }, { "name": "OS Version", "value": "14.0" }] }, + { "name": "Download Size", "value": 73297518, "meta": [{ "name": "device", "value": "iPhone11,8" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Install Size", "value": 164789269, "meta": [{ "name": "device", "value": "iPhone11,8" }, { "name": "OS Version", "value": "13.0" }] }, + { "name": "Download Size", "value": 80974447, "meta": [{ "name": "device", "value": "iPhone8,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Install Size", "value": 172389950, "meta": [{ "name": "device", "value": "iPhone8,2" }, { "name": "OS Version", "value": "15.0" }] }, + { "name": "Download Size", "value": 98758624, "meta": [{ "name": "device", "value": "Universal" }] }, + { "name": "Install Size", "value": 195563137, "meta": [{ "name": "device", "value": "Universal" }] } + ] +} From 323e622d242ff612cf40331bebd9ed2925d1514f Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 17 May 2022 17:51:53 +0200 Subject: [PATCH 03/13] android_send_app_size_metrics action + specs --- .../android/android_send_app_size_metrics.rb | 193 ++++++++++++++++++ spec/android_send_app_size_metrics_spec.rb | 137 +++++++++++++ .../android-metrics-payload.json | 26 +++ 3 files changed, 356 insertions(+) create mode 100644 lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb create mode 100644 spec/android_send_app_size_metrics_spec.rb create mode 100644 spec/test-data/app_size_metrics/android-metrics-payload.json diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb new file mode 100644 index 000000000..391e6e0a6 --- /dev/null +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb @@ -0,0 +1,193 @@ +require_relative '../../helper/app_size_metrics_helper' + +module Fastlane + module Actions + class AndroidSendAppSizeMetricsAction < Action + def self.run(params) + # Check input parameters + base_url = URI(params[:api_base_url]) + api_token = params[:api_token] + if (api_token.nil? || api_token.empty?) && !base_url.is_a?(URI::File) + UI.user_error!('An API token is required when using an `api_base_url` with a scheme other than `file://`') + end + + # Build the payload base + metrics_helper = Fastlane::WPMRT::AppSizeMetricsHelper.new( + Platform: 'Android', + 'App Name': params[:app_name], + 'App Version': params[:app_version_name], + 'Version Code': params[:app_version_code], + 'Product Flavor': params[:product_flavor], + 'Build Type': params[:build_type], + Source: params[:source] + ) + metrics_helper.add_metric(name: 'AAB File Size', value: File.size(params[:aab_path])) + + # Add device-specific 'splits' metrics to the payload if a `:include_split_sizes` is enabled + if params[:include_split_sizes] + check_bundletool_installed! + apkanalyzer_bin = find_apkanalyzer_binary! + UI.message("[App Size Metrics] Generating the various APK splits from #{params[:aab_path]}...") + Dir.mktmpdir('release-toolkit-android-app-size-metrics') do |tmp_dir| + Action.sh('bundletool', 'build-apks', '--bundle', params[:aab_path], '--output-format', 'DIRECTORY', '--output', tmp_dir) + apks = Dir.glob('splits/*.apk', base: tmp_dir).map { |f| File.join(tmp_dir, f) } + UI.message("[App Size Metrics] Generated #{apks.length} APKs.") + + apks.each do |apk| + UI.message("[App Size Metrics] Computing file and download size of #{File.basename(apk)}...") + split_name = File.basename(apk, '.apk').delete_prefix('base-') + file_size = Action.sh(apkanalyzer_bin, 'apk', 'file-size', apk, print_command: false, print_command_output: false).chomp.to_i + download_size = Action.sh(apkanalyzer_bin, 'apk', 'download-size', apk, print_command: false, print_command_output: false).chomp.to_i + metrics_helper.add_metric(name: 'APK File Size', value: file_size, meta: { split: split_name }) + metrics_helper.add_metric(name: 'Download Size', value: download_size, meta: { split: split_name }) + end + + UI.message('[App Size Metrics] Done computing splits sizes.') + end + end + + # Send the payload + metrics_helper.send_metrics( + base_url: base_url, + api_token: api_token, + use_gzip: params[:use_gzip_content_encoding] + ) + end + + def self.check_bundletool_installed! + Action.sh('command', '-v', 'bundletool', print_command: false, print_command_output: false) + rescue StandardError + UI.user_error!('bundletool is required to build the split APKs. Install it with `brew install bundletool`') + raise + end + + def self.find_apkanalyzer_binary! + sdk_root = ENV['ANDROID_SDK_ROOT'] || ENV['ANDROID_HOME'] + apkanalyzer_bin = sdk_root.nil? ? Action.sh('command', '-v', 'apkanalyzer') : File.join(sdk_root, 'cmdline-tools', 'latest', 'bin', 'apkanalyzer') + UI.user_error!('Unable to find apkanalyzer executable. Make sure you installed the Android SDK Command-line Tools') unless File.executable?(apkanalyzer_bin) + apkanalyzer_bin + end + + ##################################################### + # @!group Documentation + ##################################################### + + def self.description + 'Send Android app size metrics to our metrics server' + end + + def self.details + <<~DETAILS + Send Android app size metrics to our metrics server. + + See https://github.com/Automattic/apps-metrics for the API contract expected by the Metrics server you will send those metrics to. + + Tip: If you provide a `file://` URL for the `api_base_url`, the action will write the payload on disk at the specified path instead of sending + 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. + DETAILS + end + + def self.available_options + [ + FastlaneCore::ConfigItem.new( + key: :api_base_url, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_API_BASE_URL', + 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)', + type: String, + optional: false + ), + FastlaneCore::ConfigItem.new( + key: :api_token, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_API_TOKEN', + description: 'The bearer token to call the API. Required, unless `api_base_url` is a `file://` URL', + type: String, + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :use_gzip_content_encoding, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_USE_GZIP_CONTENT_ENCODING', + description: 'Specify that we should use `Content-Encoding: gzip` and gzip the body when sending the request', + type: FastlaneCore::Boolean, + default_value: true + ), + FastlaneCore::ConfigItem.new( + key: :app_name, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APP_NAME', + description: 'The name of the app for which we are publishing metrics, to help filter by app in the dashboard', + type: String, + optional: false + ), + FastlaneCore::ConfigItem.new( + key: :app_version_name, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APP_VERSION_NAME', + description: 'The version name of the app for which we are publishing metrics, to help filter by version in the dashboard', + type: String, + optional: false + ), + FastlaneCore::ConfigItem.new( + key: :app_version_code, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APP_VERSION_CODE', + description: 'The version code of the app for which we are publishing metrics, to help filter by version in the dashboard', + type: Integer, + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :product_flavor, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_PRODUCT_FLAVOR', + description: 'The product flavor for which we are publishing metrics, to help filter by flavor in the dashboard. E.g. `Vanilla`, `Jalapeno`, `Wasabi`', + type: String, + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :build_type, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_BUILD_TYPE', + description: 'The build type for which we are publishing metrics, to help filter by build type in the dashboard. E.g. `Debug`, `Release`', + type: String, + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :source, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_SOURCE', + description: 'The type of event at the origin of that build, to help filter data in the dashboard. E.g. `pr`, `beta`, `final-release`', + type: String, + optional: true + ), + FastlaneCore::ConfigItem.new( + key: :aab_path, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_AAB_PATH', + description: 'The path to the .aab to extract size information from', + type: String, + optional: false, + verify_block: proc do |value| + UI.user_error!('You must provide an path to an existing `.aab` file') unless File.exist?(value) + end + ), + FastlaneCore::ConfigItem.new( + key: :include_split_sizes, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_INCLUDE_SPLIT_SIZES', + description: 'Indicate if we should use `bundletool` and `apkanalyzer` to also compute and send "split apk" sizes per architecture. ' \ + + 'Setting this to `true` adds a bit of extra time to generate the `.apk` and extract the data, but provides more detailed metrics', + type: FastlaneCore::Boolean, + default_value: true + ), + ] + end + + def self.return_type + :integer + end + + def self.return_value + 'The HTTP return code from the call. Expect a 201 when new metrics were received successfully and entries created in the database' + end + + def self.authors + ['automattic'] + end + + def self.is_supported?(platform) + platform == :android + end + end + end +end diff --git a/spec/android_send_app_size_metrics_spec.rb b/spec/android_send_app_size_metrics_spec.rb new file mode 100644 index 000000000..532e49a4e --- /dev/null +++ b/spec/android_send_app_size_metrics_spec.rb @@ -0,0 +1,137 @@ +require_relative './spec_helper' + +describe Fastlane::Actions::AndroidSendAppSizeMetricsAction do + let(:test_data_dir) { File.join(File.dirname(__FILE__), 'test-data', 'app_size_metrics') } + + def test_app_size_action(fake_aab_size:, fake_apks:, expected_payload:, **other_action_args) + in_tmp_dir do |tmp_dir| + # Arrange + output_file = File.join(tmp_dir, 'output-payload') + aab_path = File.join(tmp_dir, 'fake.aab') + File.write(aab_path, '-fake-aab-file-') + allow(File).to receive(:size).with(aab_path).and_return(fake_aab_size) + + if other_action_args[:include_split_sizes] != false + # Arrange: fake that apkanalyzer exists + ENV['ANDROID_SDK_ROOT'] = '__ANDROID_SDK_ROOT__FOR_TESTS__' + apkanalyzer_bin = File.join('__ANDROID_SDK_ROOT__FOR_TESTS__', 'cmdline-tools', 'latest', 'bin', 'apkanalyzer') + allow(File).to receive(:executable?).with(apkanalyzer_bin).and_return(true) + + # Arrange: fake that bundletool exists and mock its call to create fake apks with corresponding apkanalyzer calls mocks + allow(Fastlane::Action).to receive(:sh).with('command', '-v', 'bundletool', anything) + allow(Fastlane::Action).to receive(:sh).with('bundletool', 'build-apks', '--bundle', aab_path, '--output-format', 'DIRECTORY', '--output', anything) do |*args| + bundletool_tmpdir = args.last + FileUtils.mkdir(File.join(bundletool_tmpdir, 'splits')) + fake_apks.each do |apk_name, sizes| + apk_path = File.join(bundletool_tmpdir, 'splits', apk_name.to_s) + File.write(apk_path, "Fake APK file (#{sizes})") + allow(Fastlane::Action).to receive(:sh).with(apkanalyzer_bin, 'apk', 'file-size', apk_path, anything).and_return(sizes[0].to_s) + allow(Fastlane::Action).to receive(:sh).with(apkanalyzer_bin, 'apk', 'download-size', apk_path, anything).and_return(sizes[1].to_s) + end + end + end + + # Act + code = run_described_fastlane_action( + api_base_url: File.join('file://localhost/', output_file), + aab_path: aab_path, + **other_action_args + ) + + # Asserts + expect(code).to eq(201) + expect(File).to exist(output_file) + gzip_disabled = other_action_args[:use_gzip_content_encoding] == false + generated_payload = gzip_disabled ? File.read(output_file) : Zlib::GzipReader.open(output_file, &:read) + # Compare the payloads as pretty-formatted JSON, to make the diff in test failures more readable if one happen + expect(JSON.pretty_generate(JSON.parse(generated_payload))).to eq(JSON.pretty_generate(expected_payload)), 'Decompressed JSON payload was not as expected' + # Compare the payloads as raw uncompressed data as a final check + expect(generated_payload).to eq(expected_payload.to_json) + end + end + + context 'when `include_split_sizes` is turned off' do + it 'generates the expected payload compressed by default' do + expected = { + meta: [ + { name: 'Platform', value: 'Android' }, + { name: 'App Name', value: 'my-app' }, + { name: 'App Version', value: '10.2-rc-3' }, + { name: 'Product Flavor', value: 'Vanilla' }, + { name: 'Build Type', value: 'Release' }, + { name: 'Source', value: 'unit-test' }, + ], + metrics: [ + { name: 'AAB File Size', value: 123_456 }, + ] + } + + test_app_size_action( + fake_aab_size: 123_456, + fake_apks: {}, + expected_payload: expected, + app_name: 'my-app', + app_version_name: '10.2-rc-3', + product_flavor: 'Vanilla', + build_type: 'Release', + source: 'unit-test', + include_split_sizes: false + ) + end + + it 'generates the expected payload uncompressed when disabling gzip' do + expected = { + meta: [ + { name: 'Platform', value: 'Android' }, + { name: 'App Name', value: 'my-app' }, + { name: 'App Version', value: '10.2-rc-3' }, + { name: 'Product Flavor', value: 'Vanilla' }, + { name: 'Build Type', value: 'Release' }, + { name: 'Source', value: 'unit-test' }, + ], + metrics: [ + { name: 'AAB File Size', value: 123_456 }, + ] + } + + test_app_size_action( + fake_aab_size: 123_456, + fake_apks: {}, + expected_payload: expected, + app_name: 'my-app', + app_version_name: '10.2-rc-3', + product_flavor: 'Vanilla', + build_type: 'Release', + source: 'unit-test', + include_split_sizes: false, + use_gzip_content_encoding: false + ) + end + end + + context 'when keeping the default value of `include_split_sizes` turned on' do + it 'generates the expected payload containing the aab file size and optimized split sizes' do + expected_fixture = File.join(test_data_dir, 'android-metrics-payload.json') + expected = JSON.parse(File.read(expected_fixture)) + + test_app_size_action( + fake_aab_size: 987_654_321, + fake_apks: { + 'base-arm64_v8a.apk': [164_080, 64_080], + 'base-arm64_v8a_2.apk': [164_082, 64_082], + 'base-armeabi.apk': [150_000, 50_000], + 'base-armeabi_2.apk': [150_002, 50_002], + 'base-armeabi_v7a.apk': [150_070, 50_070], + 'base-armeabi_v7a_2.apk': [150_072, 50_072] + }, + expected_payload: expected, + app_name: 'wordpress', + app_version_name: '19.8-rc-3', + app_version_code: 1214, + product_flavor: 'Vanilla', + build_type: 'Release', + source: 'unit-test' + ) + end + end +end diff --git a/spec/test-data/app_size_metrics/android-metrics-payload.json b/spec/test-data/app_size_metrics/android-metrics-payload.json new file mode 100644 index 000000000..e919aa6e7 --- /dev/null +++ b/spec/test-data/app_size_metrics/android-metrics-payload.json @@ -0,0 +1,26 @@ +{ + "meta": [ + { "name": "Platform", "value": "Android" }, + { "name": "App Name", "value": "wordpress" }, + { "name": "App Version", "value": "19.8-rc-3" }, + { "name": "Version Code", "value": 1214 }, + { "name": "Product Flavor", "value": "Vanilla" }, + { "name": "Build Type", "value": "Release" }, + { "name": "Source", "value": "unit-test" } + ], + "metrics": [ + { "name": "AAB File Size", "value": 987654321 }, + { "name": "APK File Size", "value": 164082, "meta": [{ "name": "split", "value": "arm64_v8a_2" }] }, + { "name": "Download Size", "value": 64082, "meta": [{ "name": "split", "value": "arm64_v8a_2" }] }, + { "name": "APK File Size", "value": 150072, "meta": [{ "name": "split", "value": "armeabi_v7a_2" }] }, + { "name": "Download Size", "value": 50072, "meta": [{ "name": "split", "value": "armeabi_v7a_2" }] }, + { "name": "APK File Size", "value": 150002, "meta": [{ "name": "split", "value": "armeabi_2" }] }, + { "name": "Download Size", "value": 50002, "meta": [{ "name": "split", "value": "armeabi_2" }] }, + { "name": "APK File Size", "value": 164080, "meta": [{ "name": "split", "value": "arm64_v8a" }] }, + { "name": "Download Size", "value": 64080, "meta": [{ "name": "split", "value": "arm64_v8a" }] }, + { "name": "APK File Size", "value": 150000, "meta": [{ "name": "split", "value": "armeabi" }] }, + { "name": "Download Size", "value": 50000, "meta": [{ "name": "split", "value": "armeabi" }] }, + { "name": "APK File Size", "value": 150070, "meta": [{ "name": "split", "value": "armeabi_v7a" }] }, + { "name": "Download Size", "value": 50070, "meta": [{ "name": "split", "value": "armeabi_v7a" }] } + ] +} From f1fc01a50ee52fefaa32632417acfbad833405c0 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 17 May 2022 17:52:05 +0200 Subject: [PATCH 04/13] Add CHANGELOG entry --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d46b1ba9c..9cee66a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,7 @@ _None_ * The `ios_lint_localizations` action now also checks for duplicated keys in the `.strings` files. The behavior is optional via the `check_duplicate_keys` parameter and enabled by default. [#360] - -_None_ +* Introduce new `ios_send_app_size_metrics` and `android_send_app_size_metrics` actions. [#364] ### Bug Fixes From 49e0c21b5cf51ed523d10bc69070df079d8aea7c Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 17 May 2022 20:52:03 +0200 Subject: [PATCH 05/13] Rename `api_base_url` to `api_url` Since we expect it to point to the full URL, including the `/api/grouped-metrics` path, in order to be as flexible as possible --- .../android/android_send_app_size_metrics.rb | 16 ++++++++-------- .../actions/ios/ios_send_app_size_metrics.rb | 16 ++++++++-------- .../helper/app_size_metrics_helper.rb | 6 +++--- spec/android_send_app_size_metrics_spec.rb | 2 +- spec/app_size_metrics_helper_spec.rb | 18 +++++++++--------- spec/ios_send_app_size_metrics_spec.rb | 2 +- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb index 391e6e0a6..45d39de08 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb @@ -5,10 +5,10 @@ module Actions class AndroidSendAppSizeMetricsAction < Action def self.run(params) # Check input parameters - base_url = URI(params[:api_base_url]) + api_url = URI(params[:api_url]) api_token = params[:api_token] - if (api_token.nil? || api_token.empty?) && !base_url.is_a?(URI::File) - UI.user_error!('An API token is required when using an `api_base_url` with a scheme other than `file://`') + if (api_token.nil? || api_token.empty?) && !api_url.is_a?(URI::File) + UI.user_error!('An API token is required when using an `api_url` with a scheme other than `file://`') end # Build the payload base @@ -48,7 +48,7 @@ def self.run(params) # Send the payload metrics_helper.send_metrics( - base_url: base_url, + to: api_url, api_token: api_token, use_gzip: params[:use_gzip_content_encoding] ) @@ -82,7 +82,7 @@ def self.details See https://github.com/Automattic/apps-metrics for the API contract expected by the Metrics server you will send those metrics to. - Tip: If you provide a `file://` URL for the `api_base_url`, the action will write the payload on disk at the specified path instead of sending + 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 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. DETAILS end @@ -90,8 +90,8 @@ def self.details def self.available_options [ FastlaneCore::ConfigItem.new( - key: :api_base_url, - env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_API_BASE_URL', + key: :api_url, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_API_URL', 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)', type: String, optional: false @@ -99,7 +99,7 @@ def self.available_options FastlaneCore::ConfigItem.new( key: :api_token, env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_API_TOKEN', - description: 'The bearer token to call the API. Required, unless `api_base_url` is a `file://` URL', + description: 'The bearer token to call the API. Required, unless `api_url` is a `file://` URL', type: String, optional: true ), diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb index 338255b84..8fa3fa999 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb @@ -6,10 +6,10 @@ module Actions class IosSendAppSizeMetricsAction < Action def self.run(params) # Check input parameters - base_url = URI(params[:api_base_url]) + api_url = URI(params[:api_url]) api_token = params[:api_token] - if (api_token.nil? || api_token.empty?) && !base_url.is_a?(URI::File) - UI.user_error!('An API token is required when using an `api_base_url` with a scheme other than `file://`') + if (api_token.nil? || api_token.empty?) && !api_url.is_a?(URI::File) + UI.user_error!('An API token is required when using an `api_url` with a scheme other than `file://`') end # Build the payload base @@ -38,7 +38,7 @@ def self.run(params) # Send the payload metrics_helper.send_metrics( - base_url: base_url, + to: api_url, api_token: api_token, use_gzip: params[:use_gzip_content_encoding] ) @@ -66,7 +66,7 @@ def self.details See https://github.com/Automattic/apps-metrics for the API contract expected by the Metrics server you are expected to send those metrics to. - Tip: If you provide a `file://` URL for the `api_base_url`, the action will write the payload on disk at the specified path instead of sending + 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 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. DETAILS end @@ -74,8 +74,8 @@ def self.details def self.available_options [ FastlaneCore::ConfigItem.new( - key: :api_base_url, - env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_API_BASE_URL', + key: :api_url, + env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_API_URL', 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)', type: String, optional: false @@ -83,7 +83,7 @@ def self.available_options FastlaneCore::ConfigItem.new( key: :api_token, env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_API_TOKEN', - description: 'The bearer token to call the API. Required, unless `api_base_url` is a `file://` URL', + description: 'The bearer token to call the API. Required, unless `api_url` is a `file://` URL', type: String, optional: true ), diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb index 5e3c6decb..9aa705b37 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb @@ -49,12 +49,12 @@ def to_h # # Must conform to the API described in https://github.com/Automattic/apps-metrics/wiki/Queue-Group-of-Metrics # - # @param [String,URI] base_url The base URL of the App Metrics service + # @param [String,URI] to The URL of the App Metrics service, or a `file://` URL to write the payload to disk # @param [String] api_token The API bearer token to use to register the metric. # @return [Integer] the HTTP response code # - def send_metrics(base_url:, api_token:, use_gzip: true) - uri = URI(base_url) + def send_metrics(to:, api_token:, use_gzip: true) + uri = URI(to) json_payload = use_gzip ? Zlib.gzip(to_h.to_json) : to_h.to_json # Allow using a `file:` URI for debugging diff --git a/spec/android_send_app_size_metrics_spec.rb b/spec/android_send_app_size_metrics_spec.rb index 532e49a4e..f3d387efb 100644 --- a/spec/android_send_app_size_metrics_spec.rb +++ b/spec/android_send_app_size_metrics_spec.rb @@ -33,7 +33,7 @@ def test_app_size_action(fake_aab_size:, fake_apks:, expected_payload:, **other_ # Act code = run_described_fastlane_action( - api_base_url: File.join('file://localhost/', output_file), + api_url: File.join('file://localhost/', output_file), aab_path: aab_path, **other_action_args ) diff --git a/spec/app_size_metrics_helper_spec.rb b/spec/app_size_metrics_helper_spec.rb index b9e4cebda..f0613f56e 100644 --- a/spec/app_size_metrics_helper_spec.rb +++ b/spec/app_size_metrics_helper_spec.rb @@ -79,9 +79,9 @@ it 'writes the payload uncompressed to a file when disabling gzip' do in_tmp_dir do |tmp_dir| output_file = File.join(tmp_dir, 'payload.json') - base_url = File.join('file://localhost/', output_file) + file_url = File.join('file://localhost/', output_file) - code = metrics_helper.send_metrics(base_url: base_url, api_token: nil, use_gzip: false) + code = metrics_helper.send_metrics(to: file_url, api_token: nil, use_gzip: false) expect(code).to eq(201) expect(File).to exist(output_file) @@ -93,9 +93,9 @@ it 'writes the payload compressed to a file when enabling gzip' do in_tmp_dir do |tmp_dir| output_file = File.join(tmp_dir, 'payload.json.gz') - base_url = File.join('file://localhost/', output_file) + file_url = File.join('file://localhost/', output_file) - code = metrics_helper.send_metrics(base_url: base_url, api_token: nil, use_gzip: true) + code = metrics_helper.send_metrics(to: file_url, api_token: nil, use_gzip: true) expect(code).to eq(201) expect(File).to exist(output_file) @@ -106,7 +106,7 @@ end context 'when using non-file:// scheme for the URL' do - let(:base_url) { 'https://fake-metrics-server/api/grouped-metrics' } + let(:api_url) { 'https://fake-metrics-server/api/grouped-metrics' } let(:token) { 'fake#tokn' } it 'sends the payload uncompressed to the server and with the right headers when disabling gzip' do @@ -116,11 +116,11 @@ 'Content-Type': 'application/json' } last_received_body = nil - stub = stub_request(:post, base_url).with(headers: expected_headers) do |req| + stub = stub_request(:post, api_url).with(headers: expected_headers) do |req| last_received_body = req.body end.to_return(status: 201) - code = metrics_helper.send_metrics(base_url: base_url, api_token: token, use_gzip: false) + code = metrics_helper.send_metrics(to: api_url, api_token: token, use_gzip: false) expect(code).to eq(201) expect(stub).to have_been_made.once @@ -135,11 +135,11 @@ 'Content-Encoding': 'gzip' } last_received_body = nil - stub = stub_request(:post, base_url).with(headers: expected_headers) do |req| + stub = stub_request(:post, api_url).with(headers: expected_headers) do |req| last_received_body = req.body end.to_return(status: 201) - code = metrics_helper.send_metrics(base_url: base_url, api_token: token, use_gzip: true) + code = metrics_helper.send_metrics(to: api_url, api_token: token, use_gzip: true) expect(code).to eq(201) expect(stub).to have_been_made.once diff --git a/spec/ios_send_app_size_metrics_spec.rb b/spec/ios_send_app_size_metrics_spec.rb index 95ec5ce4a..f433465e1 100644 --- a/spec/ios_send_app_size_metrics_spec.rb +++ b/spec/ios_send_app_size_metrics_spec.rb @@ -14,7 +14,7 @@ def test_app_size_action(fake_ipa_size:, expected_payload:, **other_action_args) # Act code = run_described_fastlane_action( - api_base_url: File.join('file://localhost/', output_file), + api_url: File.join('file://localhost/', output_file), ipa_path: ipa_path, **other_action_args ) From 81ae37410ffcfbd43e5196c94bd879da84cf7ffd Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Wed, 18 May 2022 15:53:24 +0200 Subject: [PATCH 06/13] Don't remove the `base-` prefix from the split name --- .../android/android_send_app_size_metrics.rb | 2 +- .../android-metrics-payload.json | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb index 45d39de08..d4a2f6ceb 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb @@ -35,7 +35,7 @@ def self.run(params) apks.each do |apk| UI.message("[App Size Metrics] Computing file and download size of #{File.basename(apk)}...") - split_name = File.basename(apk, '.apk').delete_prefix('base-') + split_name = File.basename(apk, '.apk') file_size = Action.sh(apkanalyzer_bin, 'apk', 'file-size', apk, print_command: false, print_command_output: false).chomp.to_i download_size = Action.sh(apkanalyzer_bin, 'apk', 'download-size', apk, print_command: false, print_command_output: false).chomp.to_i metrics_helper.add_metric(name: 'APK File Size', value: file_size, meta: { split: split_name }) diff --git a/spec/test-data/app_size_metrics/android-metrics-payload.json b/spec/test-data/app_size_metrics/android-metrics-payload.json index e919aa6e7..f77014b5f 100644 --- a/spec/test-data/app_size_metrics/android-metrics-payload.json +++ b/spec/test-data/app_size_metrics/android-metrics-payload.json @@ -10,17 +10,17 @@ ], "metrics": [ { "name": "AAB File Size", "value": 987654321 }, - { "name": "APK File Size", "value": 164082, "meta": [{ "name": "split", "value": "arm64_v8a_2" }] }, - { "name": "Download Size", "value": 64082, "meta": [{ "name": "split", "value": "arm64_v8a_2" }] }, - { "name": "APK File Size", "value": 150072, "meta": [{ "name": "split", "value": "armeabi_v7a_2" }] }, - { "name": "Download Size", "value": 50072, "meta": [{ "name": "split", "value": "armeabi_v7a_2" }] }, - { "name": "APK File Size", "value": 150002, "meta": [{ "name": "split", "value": "armeabi_2" }] }, - { "name": "Download Size", "value": 50002, "meta": [{ "name": "split", "value": "armeabi_2" }] }, - { "name": "APK File Size", "value": 164080, "meta": [{ "name": "split", "value": "arm64_v8a" }] }, - { "name": "Download Size", "value": 64080, "meta": [{ "name": "split", "value": "arm64_v8a" }] }, - { "name": "APK File Size", "value": 150000, "meta": [{ "name": "split", "value": "armeabi" }] }, - { "name": "Download Size", "value": 50000, "meta": [{ "name": "split", "value": "armeabi" }] }, - { "name": "APK File Size", "value": 150070, "meta": [{ "name": "split", "value": "armeabi_v7a" }] }, - { "name": "Download Size", "value": 50070, "meta": [{ "name": "split", "value": "armeabi_v7a" }] } + { "name": "APK File Size", "value": 164082, "meta": [{ "name": "split", "value": "base-arm64_v8a_2" }] }, + { "name": "Download Size", "value": 64082, "meta": [{ "name": "split", "value": "base-arm64_v8a_2" }] }, + { "name": "APK File Size", "value": 150072, "meta": [{ "name": "split", "value": "base-armeabi_v7a_2" }] }, + { "name": "Download Size", "value": 50072, "meta": [{ "name": "split", "value": "base-armeabi_v7a_2" }] }, + { "name": "APK File Size", "value": 150002, "meta": [{ "name": "split", "value": "base-armeabi_2" }] }, + { "name": "Download Size", "value": 50002, "meta": [{ "name": "split", "value": "base-armeabi_2" }] }, + { "name": "APK File Size", "value": 164080, "meta": [{ "name": "split", "value": "base-arm64_v8a" }] }, + { "name": "Download Size", "value": 64080, "meta": [{ "name": "split", "value": "base-arm64_v8a" }] }, + { "name": "APK File Size", "value": 150000, "meta": [{ "name": "split", "value": "base-armeabi" }] }, + { "name": "Download Size", "value": 50000, "meta": [{ "name": "split", "value": "base-armeabi" }] }, + { "name": "APK File Size", "value": 150070, "meta": [{ "name": "split", "value": "base-armeabi_v7a" }] }, + { "name": "Download Size", "value": 50070, "meta": [{ "name": "split", "value": "base-armeabi_v7a" }] } ] } From f804caf7ba067a57c3e1304a65d424b8403abbd7 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Thu, 19 May 2022 19:20:16 +0200 Subject: [PATCH 07/13] Use UI.success instead of UI.message --- .../plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb index 9aa705b37..d3841908a 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb @@ -80,7 +80,7 @@ def send_metrics(to:, api_token:, use_gzip: true) end if response.is_a?(Net::HTTPSuccess) - UI.message("Metrics sent. (#{response.code} #{response.message})") + UI.success("Metrics sent. (#{response.code} #{response.message})") else UI.error("Metrics failed to send. Received: #{response.code} #{response.message}") UI.message("Request was #{request.method} to #{request.uri}") From 41b6e8ec336834a561b7c99913d92dade8388209 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 24 May 2022 12:11:52 +0200 Subject: [PATCH 08/13] Apply suggestions from code review Co-authored-by: Gio Lodi --- .../actions/ios/ios_send_app_size_metrics.rb | 12 ++++++------ .../helper/app_size_metrics_helper.rb | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb index 8fa3fa999..340f42959 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb @@ -97,35 +97,35 @@ def self.available_options FastlaneCore::ConfigItem.new( key: :app_name, env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_APP_NAME', - description: 'The name of the app for which we are publishing metrics, to help filter by app in the dashboard', + description: 'The name of the app for which we are publishing metrics, to help with filtering and grouping', type: String, optional: false ), FastlaneCore::ConfigItem.new( key: :app_version, env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_APP_VERSION', - description: 'The version of the app for which we are publishing metrics, to help filter by version in the dashboard', + description: 'The version of the app for which we are publishing metrics, to help with filtering and grouping', type: String, optional: false ), FastlaneCore::ConfigItem.new( key: :build_type, env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_BUILD_TYPE', - description: 'The build configuration for which we are publishing metrics, to help filter by build config in the dashboard. E.g. `Debug`, `Release`', + description: 'The build configuration for which we are publishing metrics, to help with filtering and grouping. E.g. `Debug`, `Release`', type: String, optional: true ), FastlaneCore::ConfigItem.new( key: :source, env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_SOURCE', - description: 'The type of event at the origin of that build, to help filter data in the dashboard. E.g. `pr`, `beta`, `final-release`', + description: 'The type of event at the origin of that build, to help with filtering and grouping. E.g. `pr`, `beta`, `final-release`', type: String, optional: true ), FastlaneCore::ConfigItem.new( key: :ipa_path, env_name: 'FL_IOS_SEND_APP_SIZE_METRICS_IPA_PATH', - description: 'The path to the .ipa to extract size information from', + description: 'The path to the `.ipa` to extract size information from', type: String, optional: false, default_value: Actions.lane_context[SharedValues::IPA_OUTPUT_PATH], @@ -158,7 +158,7 @@ def self.authors end def self.is_supported?(platform) - [:ios, :mac].include? platform + platform == :ios end end end diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb index d3841908a..2b2a81d8c 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb @@ -59,7 +59,7 @@ def send_metrics(to:, api_token:, use_gzip: true) # Allow using a `file:` URI for debugging if uri.is_a?(URI::File) - UI.message("Writing metrics payload to file #{uri.path} (instead of sending it to a server)") + UI.message("Writing metrics payload to file #{uri.path} (instead of sending it to a remote API endpoint)") File.write(uri.path, json_payload) return 201 # To make it easy at call site to check for pseudo-status code 200 even in non-HTTP cases end From b447780c3247095681ba171aa5eaea207cdee52d Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 24 May 2022 12:12:13 +0200 Subject: [PATCH 09/13] FIx comment mismatching code --- .../plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb index 2b2a81d8c..ca024e159 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb @@ -61,7 +61,7 @@ def send_metrics(to:, api_token:, use_gzip: true) if uri.is_a?(URI::File) UI.message("Writing metrics payload to file #{uri.path} (instead of sending it to a remote API endpoint)") File.write(uri.path, json_payload) - return 201 # To make it easy at call site to check for pseudo-status code 200 even in non-HTTP cases + return 201 # To make it easy at call site to check for pseudo-status code 201 even in non-HTTP cases end UI.message("Sending metrics to #{uri}...") From a3660c8f12a45c8a5d7c824cd18ac347ee1055cf Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 24 May 2022 12:15:25 +0200 Subject: [PATCH 10/13] Switch code style for meta `nil` removal To make it more readable See also https://github.com/wordpress-mobile/release-toolkit/pull/364#discussion_r880303387 --- .../plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb index ca024e159..4ba1ca442 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb @@ -33,7 +33,7 @@ def meta=(hash) # def add_metric(name:, value:, meta: nil) metric = { name: name, value: value } - meta = (meta || {}).compact # Remove nil values if any + meta = meta&.compact || {} # Remove nil values if any (and use empty Hash if nil was provided) metric[:meta] = meta.map { |meta_key, meta_value| { name: meta_key.to_s, value: meta_value } } unless meta.empty? @metrics.append(metric) end From fc90b313e29a55d4fc621e705ce636f598593bbe Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 24 May 2022 12:18:20 +0200 Subject: [PATCH 11/13] Rename module namespace for helper --- .../actions/android/android_send_app_size_metrics.rb | 2 +- .../wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb | 2 +- .../plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb | 2 +- spec/app_size_metrics_helper_spec.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb index d4a2f6ceb..6e4d6e97a 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb @@ -12,7 +12,7 @@ def self.run(params) end # Build the payload base - metrics_helper = Fastlane::WPMRT::AppSizeMetricsHelper.new( + metrics_helper = Fastlane::Helper::AppSizeMetricsHelper.new( Platform: 'Android', 'App Name': params[:app_name], 'App Version': params[:app_version_name], diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb index 340f42959..c48621f30 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb @@ -13,7 +13,7 @@ def self.run(params) end # Build the payload base - metrics_helper = Fastlane::WPMRT::AppSizeMetricsHelper.new( + metrics_helper = Fastlane::Helper::AppSizeMetricsHelper.new( Platform: 'iOS', 'App Name': params[:app_name], 'App Version': params[:app_version], diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb index 4ba1ca442..33dbe7e9f 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb @@ -3,7 +3,7 @@ require 'zlib' module Fastlane - module WPMRT + module Helper # A helper class to build an App Size Metrics payload and send it to a server (or write it to disk) # # The payload generated (and sent) by this helper conforms to the API for grouped metrics described in diff --git a/spec/app_size_metrics_helper_spec.rb b/spec/app_size_metrics_helper_spec.rb index f0613f56e..e0c3a5e38 100644 --- a/spec/app_size_metrics_helper_spec.rb +++ b/spec/app_size_metrics_helper_spec.rb @@ -1,6 +1,6 @@ require_relative './spec_helper' -describe Fastlane::WPMRT::AppSizeMetricsHelper do +describe Fastlane::Helper::AppSizeMetricsHelper do describe '#to_h' do it 'generates the right payload from raw data' do metrics_helper = described_class.new({ From e8acd9d328088d94a36692a7c3bb25d227adb5a5 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 24 May 2022 12:33:10 +0200 Subject: [PATCH 12/13] Rename `meta` to `metadata` in helper API --- .../android/android_send_app_size_metrics.rb | 4 ++-- .../actions/ios/ios_send_app_size_metrics.rb | 4 ++-- .../helper/app_size_metrics_helper.rb | 20 +++++++++---------- spec/app_size_metrics_helper_spec.rb | 14 ++++++------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb index 6e4d6e97a..51d4d25af 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb @@ -38,8 +38,8 @@ def self.run(params) split_name = File.basename(apk, '.apk') file_size = Action.sh(apkanalyzer_bin, 'apk', 'file-size', apk, print_command: false, print_command_output: false).chomp.to_i download_size = Action.sh(apkanalyzer_bin, 'apk', 'download-size', apk, print_command: false, print_command_output: false).chomp.to_i - metrics_helper.add_metric(name: 'APK File Size', value: file_size, meta: { split: split_name }) - metrics_helper.add_metric(name: 'Download Size', value: download_size, meta: { split: split_name }) + metrics_helper.add_metric(name: 'APK File Size', value: file_size, metadata: { split: split_name }) + metrics_helper.add_metric(name: 'Download Size', value: download_size, metadata: { split: split_name }) end UI.message('[App Size Metrics] Done computing splits sizes.') diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb index c48621f30..b01f54908 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/ios/ios_send_app_size_metrics.rb @@ -30,8 +30,8 @@ def self.run(params) variant_descriptors = variant['variantDescriptors'] || [{ 'device' => 'Universal' }] variant_descriptors.each do |desc| variant_metadata = { device: desc['device'], 'OS Version': desc['os-version'] } - metrics_helper.add_metric(name: 'Download Size', value: variant['sizeCompressedApp'], meta: variant_metadata) - metrics_helper.add_metric(name: 'Install Size', value: variant['sizeUncompressedApp'], meta: variant_metadata) + metrics_helper.add_metric(name: 'Download Size', value: variant['sizeCompressedApp'], metadata: variant_metadata) + metrics_helper.add_metric(name: 'Install Size', value: variant['sizeUncompressedApp'], metadata: variant_metadata) end end end diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb index 33dbe7e9f..f200a160a 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/helper/app_size_metrics_helper.rb @@ -10,10 +10,10 @@ module Helper # https://github.com/Automattic/apps-metrics # class AppSizeMetricsHelper - # @param [Hash] group_meta Metadata common to all the metrics. Can be any arbitrary set of key/value pairs. + # @param [Hash] metadata Metadata common to all the metrics. Can be any arbitrary set of key/value pairs. # - def initialize(group_meta = {}) - self.meta = group_meta + def initialize(metadata = {}) + self.metadata = metadata @metrics = [] end @@ -21,26 +21,26 @@ def initialize(group_meta = {}) # # @param [Hash] hash The metadata common to all the metrics of the payload built by that helper instance. Can be any arbitrary set of key/value pairs # - def meta=(hash) - @meta = (hash.compact || {}).map { |key, value| { name: key.to_s, value: value } } + def metadata=(hash) + @metadata = (hash.compact || {}).map { |key, value| { name: key.to_s, value: value } } end # Adds a single metric to the group of metrics # # @param [String] name The metric name # @param [Integer] value The metric value - # @param [Hash] meta The arbitrary dictionary of metadata to associate to that metric entry + # @param [Hash] metadata The arbitrary dictionary of metadata to associate to that metric entry # - def add_metric(name:, value:, meta: nil) + def add_metric(name:, value:, metadata: nil) metric = { name: name, value: value } - meta = meta&.compact || {} # Remove nil values if any (and use empty Hash if nil was provided) - metric[:meta] = meta.map { |meta_key, meta_value| { name: meta_key.to_s, value: meta_value } } unless meta.empty? + metadata = metadata&.compact || {} # Remove nil values if any (and use empty Hash if nil was provided) + metric[:meta] = metadata.map { |meta_key, meta_value| { name: meta_key.to_s, value: meta_value } } unless metadata.empty? @metrics.append(metric) end def to_h { - meta: @meta, + meta: @metadata, metrics: @metrics } end diff --git a/spec/app_size_metrics_helper_spec.rb b/spec/app_size_metrics_helper_spec.rb index e0c3a5e38..83434cf78 100644 --- a/spec/app_size_metrics_helper_spec.rb +++ b/spec/app_size_metrics_helper_spec.rb @@ -7,9 +7,9 @@ 'Group Metadata 1': 'Group Value 1', 'Group Metadata 2': 'Group Value 2' }) - metrics_helper.add_metric(name: 'Metric 1', value: 12_345, meta: { m1a: 'Metric 1 Metadata A' }) + metrics_helper.add_metric(name: 'Metric 1', value: 12_345, metadata: { m1a: 'Metric 1 Metadata A' }) metrics_helper.add_metric(name: 'Metric 2', value: 67_890) - metrics_helper.add_metric(name: 'Metric 3', value: 13_579, meta: { m3a: 'Metric 3 Metadata A', m3b: 'Metric 3 Metadata B' }) + metrics_helper.add_metric(name: 'Metric 3', value: 13_579, metadata: { m3a: 'Metric 3 Metadata A', m3b: 'Metric 3 Metadata B' }) expected_hash = { meta: [ @@ -31,9 +31,9 @@ 'Group Metadata 2': nil, 'Group Metadata 3': 'Group Value 3' }) - metrics_helper.add_metric(name: 'Metric 1', value: 12_345, meta: { m1a: 'Metric 1 Metadata A', m1b: nil, m1c: 'Metric 1 Metadata C' }) - metrics_helper.add_metric(name: 'Metric 2', value: 67_890, meta: { m2a: nil }) - metrics_helper.add_metric(name: 'Metric 3', value: 13_579, meta: { m3a: 'Metric 3 Metadata A', m3b: 'Metric 3 Metadata B' }) + metrics_helper.add_metric(name: 'Metric 1', value: 12_345, metadata: { m1a: 'Metric 1 Metadata A', m1b: nil, m1c: 'Metric 1 Metadata C' }) + metrics_helper.add_metric(name: 'Metric 2', value: 67_890, metadata: { m2a: nil }) + metrics_helper.add_metric(name: 'Metric 3', value: 13_579, metadata: { m3a: 'Metric 3 Metadata A', m3b: 'Metric 3 Metadata B' }) expected_hash = { meta: [ @@ -56,9 +56,9 @@ 'Group Metadata 1': 'Group Value 1', 'Group Metadata 2': 'Group Value 2' }) - metrics_helper.add_metric(name: 'Metric 1', value: 12_345, meta: { m1a: 'Metric 1 Metadata A' }) + metrics_helper.add_metric(name: 'Metric 1', value: 12_345, metadata: { m1a: 'Metric 1 Metadata A' }) metrics_helper.add_metric(name: 'Metric 2', value: 67_890) - metrics_helper.add_metric(name: 'Metric 3', value: 13_579, meta: { m3a: 'Metric 3 Metadata A', m3b: 'Metric 3 Metadata B' }) + metrics_helper.add_metric(name: 'Metric 3', value: 13_579, metadata: { m3a: 'Metric 3 Metadata A', m3b: 'Metric 3 Metadata B' }) metrics_helper end let(:expected_data) do From ad5935cbc3e42410e030300630dc1939ef1ae714 Mon Sep 17 00:00:00 2001 From: Olivier Halligon Date: Tue, 24 May 2022 16:55:32 +0200 Subject: [PATCH 13/13] Update logic for finding (or provide) apkanalyzer --- .../android/android_send_app_size_metrics.rb | 27 ++++++++++++++++--- spec/android_send_app_size_metrics_spec.rb | 2 +- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb index 51d4d25af..3dbec910f 100644 --- a/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb +++ b/lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_send_app_size_metrics.rb @@ -26,7 +26,7 @@ def self.run(params) # Add device-specific 'splits' metrics to the payload if a `:include_split_sizes` is enabled if params[:include_split_sizes] check_bundletool_installed! - apkanalyzer_bin = find_apkanalyzer_binary! + apkanalyzer_bin = params[:apkanalyzer_binary] || find_apkanalyzer_binary! UI.message("[App Size Metrics] Generating the various APK splits from #{params[:aab_path]}...") Dir.mktmpdir('release-toolkit-android-app-size-metrics') do |tmp_dir| Action.sh('bundletool', 'build-apks', '--bundle', params[:aab_path], '--output-format', 'DIRECTORY', '--output', tmp_dir) @@ -61,10 +61,18 @@ def self.check_bundletool_installed! raise end - def self.find_apkanalyzer_binary! + def self.find_apkanalyzer_binary sdk_root = ENV['ANDROID_SDK_ROOT'] || ENV['ANDROID_HOME'] - apkanalyzer_bin = sdk_root.nil? ? Action.sh('command', '-v', 'apkanalyzer') : File.join(sdk_root, 'cmdline-tools', 'latest', 'bin', 'apkanalyzer') - UI.user_error!('Unable to find apkanalyzer executable. Make sure you installed the Android SDK Command-line Tools') unless File.executable?(apkanalyzer_bin) + if sdk_root + pattern = File.join(sdk_root, 'cmdline-tools', '{latest,tools}', 'bin', 'apkanalyzer') + apkanalyzer_bin = Dir.glob(pattern).find { |path| File.executable?(path) } + end + apkanalyzer_bin || Action.sh('command', '-v', 'apkanalyzer', print_command_output: false) { |_| nil } + end + + def self.find_apkanalyzer_binary! + apkanalyzer_bin = find_apkanalyzer_binary + 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? apkanalyzer_bin end @@ -170,6 +178,17 @@ def self.available_options type: FastlaneCore::Boolean, default_value: true ), + FastlaneCore::ConfigItem.new( + key: :apkanalyzer_binary, + env_name: 'FL_ANDROID_SEND_APP_SIZE_METRICS_APKANALYZER_BINARY', + 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', + type: String, + default_value: find_apkanalyzer_binary, + default_value_dynamic: true, + verify_block: proc do |value| + UI.user_error!('You must provide a path to an existing executable for `apkanalyzer`') unless File.executable?(value) + end + ), ] end diff --git a/spec/android_send_app_size_metrics_spec.rb b/spec/android_send_app_size_metrics_spec.rb index f3d387efb..9a51b8a58 100644 --- a/spec/android_send_app_size_metrics_spec.rb +++ b/spec/android_send_app_size_metrics_spec.rb @@ -13,8 +13,8 @@ def test_app_size_action(fake_aab_size:, fake_apks:, expected_payload:, **other_ if other_action_args[:include_split_sizes] != false # Arrange: fake that apkanalyzer exists - ENV['ANDROID_SDK_ROOT'] = '__ANDROID_SDK_ROOT__FOR_TESTS__' apkanalyzer_bin = File.join('__ANDROID_SDK_ROOT__FOR_TESTS__', 'cmdline-tools', 'latest', 'bin', 'apkanalyzer') + allow(described_class).to receive(:find_apkanalyzer_binary).and_return(apkanalyzer_bin) allow(File).to receive(:executable?).with(apkanalyzer_bin).and_return(true) # Arrange: fake that bundletool exists and mock its call to create fake apks with corresponding apkanalyzer calls mocks