xc_metrics_aggregator 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ module XcMetricsAggregator
2
+ module OutputFormat
3
+ CSV = "csv"
4
+ ASCII = "ascii"
5
+
6
+ def self.all
7
+ self.constants.map{|name| self.const_get(name) }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,113 @@
1
+ require 'etc'
2
+ require 'json'
3
+ require 'pathname'
4
+
5
+
6
+ module XcMetricsAggregator
7
+ PRODUCT_PATH = File.join('/', 'Users', Etc.getlogin, 'Library', 'Developer' , 'Xcode', 'Products')
8
+
9
+ class Product
10
+ def metrics_dir
11
+ Pathname.new(File.join(@path, 'Metrics'))
12
+ end
13
+
14
+ def metrics_file
15
+ Pathname.new(File.join(metrics_dir, 'AppStore/Metrics.xcmetricsdata'))
16
+ end
17
+
18
+ def bundle_id
19
+ Pathname.new(@path).basename.to_s
20
+ end
21
+
22
+ def has_metrics?
23
+ File.exists?(metrics_dir)
24
+ end
25
+
26
+ def initialize(path)
27
+ @path = path
28
+ end
29
+
30
+ def open
31
+ FileNotFoundException.new("File not Found: #{metrics_file}") unless has_metrics?
32
+ File.open(metrics_file) do |file|
33
+ yield JSON.load(file, symbolize_names: true)
34
+ end
35
+ end
36
+
37
+
38
+ def try_to_open
39
+ return unless has_metrics?
40
+ File.open(metrics_file) do |file|
41
+ yield JSON.load(file, symbolize_names: true)
42
+ end
43
+ end
44
+ end
45
+
46
+ class ProductsService
47
+ attr_reader :products
48
+
49
+ def initialize
50
+ @products = Dir.glob(PRODUCT_PATH + "/*").map { |dir_path| Product.new dir_path }
51
+ end
52
+
53
+ def targets(bundle_ids=[])
54
+ if bundle_ids.empty?
55
+ products
56
+ else
57
+ products.select do |product|
58
+ bundle_ids.include? product.bundle_id.to_s
59
+ end
60
+ end
61
+ end
62
+
63
+ def target(bundle_id)
64
+ if bundle_id.nil?
65
+ raise StandardError.new("needs bundle id")
66
+ end
67
+
68
+ products.select { |product| bundle_id == product.bundle_id.to_s }.first
69
+ end
70
+
71
+ def each_product(bundle_ids=[])
72
+ targets(bundle_ids).each do |product|
73
+ yield product
74
+ end
75
+ end
76
+
77
+ def structure(available_path)
78
+ structure = XcMetricsAggregator::TableStructure.new
79
+ structure.title = "available app list"
80
+ structure.headings = headings(available_path)
81
+ structure.rows = rows(available_path)
82
+ structure
83
+ end
84
+
85
+ private
86
+ def headings(available_path)
87
+ if available_path
88
+ ['bundle id', 'status', 'raw data path']
89
+ else
90
+ ['bundle id', 'status']
91
+ end
92
+
93
+ end
94
+
95
+ def rows(available_path)
96
+ rows = []
97
+ products.each do |product|
98
+ status =
99
+ if product.has_metrics?
100
+ "has metrics"
101
+ else
102
+ "fail to get metrics"
103
+ end
104
+ if available_path
105
+ rows << [product.bundle_id, status, product.metrics_file.to_s]
106
+ else
107
+ rows << [product.bundle_id, status]
108
+ end
109
+ end
110
+ return rows
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,146 @@
1
+ require 'xc_metrics_aggregator/structure/structure'
2
+
3
+ module XcMetricsAggregator
4
+ class CategoriesService
5
+ attr_reader :device_service, :percentile_service
6
+
7
+ def initialize(bundle_id, json)
8
+ @json = json
9
+ @bundle_id = bundle_id
10
+ @device_service = DevicesService.new bundle_id, json
11
+ @percentile_service = PercentilesService.new bundle_id, json
12
+ end
13
+
14
+ def structure
15
+ structure = XcMetricsAggregator::TableStructure.new
16
+ structure.title = @bundle_id
17
+ structure.headings = headings
18
+ structure.rows = rows
19
+ structure
20
+ end
21
+
22
+ def categories
23
+ @json["categories"].map { |json| Category.new json }
24
+ end
25
+
26
+ def get_dataset(section_name, device, percentile)
27
+ section = categories.map { |category| category.sections.find { |section| section.display_name == section_name } }.first
28
+ dataset = section.datasets.find do |dataset|
29
+ dataset.filter_criteria.device == device.identifier \
30
+ && dataset.filter_criteria.percentile == percentile.identifier
31
+ end
32
+ end
33
+
34
+
35
+ def get_available_dataset(section_name, device, percentile)
36
+ section = categories.map { |category|
37
+ category.sections.find { |section|
38
+ section.display_name == section_name
39
+ }
40
+ }.flatten.compact.first
41
+ if section.nil?
42
+ return nil
43
+ end
44
+
45
+ datasets = section.datasets.select do |dataset|
46
+ available_device = device ? dataset.filter_criteria.device == device.identifier : true
47
+ available_percentile = percentile ? dataset.filter_criteria.percentile == percentile.identifier : true
48
+ available_device && available_percentile && !dataset.points.empty?
49
+ end.last
50
+ datasets
51
+ end
52
+
53
+ def get_section(section_name)
54
+ section = categories.map { |category|
55
+ category.sections.find { |section|
56
+ section.display_name == section_name
57
+ }
58
+ }.flatten.compact.first
59
+ section
60
+ end
61
+
62
+ private
63
+ def rows
64
+ categories.map do |category|
65
+ [
66
+ category.display_name,
67
+ category.sections.map{ |s| s.display_name }.join("\n"),
68
+ category.sections.map{ |s| s.unit.display_name }.join("\n")
69
+ ]
70
+ end
71
+ end
72
+
73
+ def headings
74
+ ["category", "section", "unit"]
75
+ end
76
+ end
77
+ end
78
+
79
+
80
+ module XcMetricsAggregator
81
+ class Category
82
+ attr_accessor :sections, :display_name, :identifier
83
+
84
+ def initialize(json)
85
+ @sections = json["sections"].map { |section| Section.new section }
86
+ @display_name = json["displayName"]
87
+ @identifier = json["identifier"]
88
+ end
89
+ end
90
+
91
+ class Section
92
+ attr_accessor :datasets, :display_name, :identifier, :unit
93
+
94
+ def initialize(json)
95
+ @datasets = json["datasets"].map { |d| DataSet.new d }
96
+ @unit = Unit.new json["unit"]
97
+ @display_name = json["displayName"]
98
+ @identifier = json["identifier"]
99
+ end
100
+ end
101
+
102
+ class Unit
103
+ attr_accessor :display_name, :identifier
104
+
105
+ def initialize(json)
106
+ @display_name = json["displayName"]
107
+ @identifier = json["identifier"]
108
+ end
109
+ end
110
+
111
+ class FilterCriteria
112
+ attr_accessor :device, :percentile
113
+
114
+ def initialize(json)
115
+ @device = json["device"]
116
+ @percentile = json["percentile"]
117
+ end
118
+
119
+ def self.new_from_prop(device, percentile)
120
+ self.new({"device": device.identifier, "percentile": percentile.identifier})
121
+ end
122
+
123
+ def ==(other)
124
+ device == other.device && percentile == other.percentile
125
+ end
126
+ end
127
+
128
+ class Point
129
+ attr_accessor :summary, :version, :percentage_breakdown
130
+
131
+ def initialize(json)
132
+ @summary = json["summary"]
133
+ @version = json["version"]
134
+ @percentage_breakdown = json["percentageBreakdown"]
135
+ end
136
+ end
137
+
138
+ class DataSet
139
+ attr_accessor :points, :filter_criteria
140
+
141
+ def initialize(json)
142
+ @points = json["points"].map { |p| Point.new p }
143
+ @filter_criteria = FilterCriteria.new json["filterCriteria"]
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,85 @@
1
+ require 'xc_metrics_aggregator/structure/structure'
2
+
3
+ module XcMetricsAggregator
4
+ class Device
5
+ attr_accessor :is_represented, :display_name, :identifier
6
+
7
+ def initialize(json)
8
+ @is_represented = json["isRepresented"]
9
+ @display_name = json["displayName"]
10
+ @identifier = json["identifier"]
11
+ end
12
+ end
13
+
14
+ class DeviceFamily
15
+ attr_accessor :is_represented, :display_name, :identifier, :devices
16
+
17
+ def initialize(json)
18
+ @is_represented = json["isRepresented"]
19
+ @display_name = json["displayName"]
20
+ @identifier = json["identifier"]
21
+ @devices = json["devices"].map { |json| Device.new json }
22
+ end
23
+ end
24
+
25
+
26
+ class DevicesService
27
+ def initialize(bundle_id, json)
28
+ @json = json
29
+ @bundle_id = bundle_id
30
+ end
31
+
32
+ def devicefamilies
33
+ device_families_json = @json["filterCriteriaSets"]["deviceFamilies"]
34
+ device_families_json.map do |device_family_json|
35
+ DeviceFamily.new device_family_json
36
+ end
37
+ end
38
+
39
+ def structure
40
+ structure = XcMetricsAggregator::TableStructure.new
41
+ structure.title = @bundle_id
42
+ structure.headings = headings()
43
+ structure.rows = rows()
44
+ structure
45
+ end
46
+
47
+ def get_device(identifier)
48
+ if identifier.nil?
49
+ nil
50
+ end
51
+
52
+ device = devicefamilies.map do |devicefamily|
53
+ if devicefamily.identifier == identifier
54
+ return devicefamily
55
+ end
56
+
57
+ devicefamily.devices.select do |device|
58
+ device.identifier == identifier
59
+ end
60
+ end.flatten.first
61
+
62
+ device
63
+ end
64
+
65
+ private
66
+ def rows
67
+ rows = []
68
+ devicefamilies.each_with_index do |devicefamily, idx|
69
+ device_display_names = devicefamily.devices.map{ |d| d.display_name }.join("\n")
70
+ device_identifiers = devicefamily.devices.map{ |d| d.identifier }.join("\n")
71
+ row = [devicefamily.display_name, device_display_names, device_identifiers]
72
+ rows += if idx == devicefamilies.count - 1
73
+ [row]
74
+ else
75
+ [row] + [:separator]
76
+ end
77
+ end
78
+ return rows
79
+ end
80
+
81
+ def headings
82
+ ["kind", "device", "id"]
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,91 @@
1
+ require 'xc_metrics_aggregator/structure/structure'
2
+
3
+ module XcMetricsAggregator
4
+ class LatestService
5
+ def initialize(section, deviceid, percentileid)
6
+ target_datasets = {}
7
+ unit_label = ""
8
+ each_product do |product|
9
+ product.try_to_open do |json|
10
+ device = get_device(product, json, deviceid)
11
+ percentile = get_percentile(product, json, percentileid)
12
+
13
+ if device && percentile
14
+ dataset = get_dataset(product, json, section, device, percentile)
15
+ else
16
+ dataset = get_available_dataset(product, json, section, device, percentile)
17
+ end
18
+
19
+ unless dataset.nil? || dataset.points.empty?
20
+ unit_label = get_unit_label(product, json, section)
21
+ target_datasets[product.bundle_id] = dataset
22
+ end
23
+ end
24
+ end
25
+ @unit_label = unit_label
26
+ @target_datasets = target_datasets
27
+ end
28
+
29
+ def structure
30
+ structure = XcMetricsAggregator::ChartStructure.new
31
+ structure.series = series
32
+ structure.samples = samples
33
+ structure.unit = @unit_label
34
+ return structure
35
+ end
36
+
37
+ private
38
+ def samples
39
+ @target_datasets.keys.each_with_index.reduce([]) do |sum, (bundle_id, idx)|
40
+ dataset = @target_datasets[bundle_id]
41
+ if dataset.points.empty?
42
+ sum
43
+ else
44
+ latest_point = dataset.points.last
45
+ sum + [[idx, latest_point.summary]]
46
+ end
47
+ end
48
+ end
49
+
50
+ def series
51
+ structure = XcMetricsAggregator::TableStructure.new
52
+ structure.headings = ["Label", "Bundle ID", "Version"]
53
+ structure.rows = @target_datasets.keys.each_with_index.map do |bundle_id, idx|
54
+ latest_point = @target_datasets[bundle_id].points.last
55
+ [idx, bundle_id, latest_point.version]
56
+ end
57
+ structure
58
+ end
59
+
60
+ private
61
+ def each_product
62
+ XcMetricsAggregator::ProductsService.new.each_product do |product|
63
+ yield product
64
+ end
65
+ end
66
+
67
+ def get_device(product, json, deviceid)
68
+ DevicesService.new(product.bundle_id, json).get_device deviceid
69
+ end
70
+
71
+ def get_percentile(product, json, percentileid)
72
+ PercentilesService.new(product.bundle_id, json).get_percentile percentileid
73
+ end
74
+
75
+ def get_category_service(product, json)
76
+ CategoriesService.new(product.bundle_id, json)
77
+ end
78
+
79
+ def get_dataset(product, json, section, device, percentile)
80
+ get_category_service(product, json).get_dataset section, device, percentile
81
+ end
82
+
83
+ def get_available_dataset(product, json, section, device, percentile)
84
+ get_category_service(product, json).get_available_dataset section, device, percentile
85
+ end
86
+
87
+ def get_unit_label(product, json, section)
88
+ get_category_service(product, json).get_section(section).unit.display_name
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,51 @@
1
+ require 'xc_metrics_aggregator/service/categories_service'
2
+ require 'xc_metrics_aggregator/structure/structure'
3
+
4
+ module XcMetricsAggregator
5
+ class MetricsService
6
+ def initialize(bundle_id, json)
7
+ @bundle_id = bundle_id
8
+ @category_service = CategoriesService.new bundle_id, json
9
+ end
10
+
11
+ def structures(section_name, device_id, percentile_id, version)
12
+ rows = []
13
+ samples = []
14
+ index = 0
15
+ datasets(section_name).each do |dataset|
16
+ device_identifier = dataset.filter_criteria.device
17
+ validated_device = !device_id || device_identifier == device_id
18
+ percentile_identifier = dataset.filter_criteria.percentile
19
+ validated_percentile = !percentile_id || percentile_identifier == percentile_id
20
+
21
+ unless validated_device && validated_percentile
22
+ next
23
+ end
24
+ percentile = @category_service.percentile_service.get_percentile dataset.filter_criteria.percentile
25
+ device = @category_service.device_service.get_device dataset.filter_criteria.device
26
+ rows += dataset.points.map.with_index(index) { |p, i| [i, p.version, device.display_name, percentile.display_name] }
27
+ samples += dataset.points.map.with_index(index) { |p, i| [i, p.summary] }
28
+ index += dataset.points.count
29
+ end
30
+
31
+ table_structure = XcMetricsAggregator::TableStructure.new
32
+ table_structure.headings = ["Label", "Version", "Device", "Percentile"]
33
+ table_structure.title = section_name
34
+ table_structure.rows = rows
35
+ structure = XcMetricsAggregator::ChartStructure.new
36
+ structure.series = table_structure
37
+ structure.unit = @category_service.get_section(section_name).unit.display_name
38
+ structure.samples = samples
39
+
40
+ structure
41
+ end
42
+
43
+ def datasets(section_name)
44
+ section = @category_service.get_section(section_name)
45
+ unless section
46
+ raise StandardError.new("wrong section name")
47
+ end
48
+ section.datasets
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,52 @@
1
+ require 'xc_metrics_aggregator/structure/structure'
2
+
3
+ module XcMetricsAggregator
4
+ class Percentile
5
+ attr_accessor :display_name, :is_represented, :identifier
6
+
7
+ def initialize(json)
8
+ @is_represented = json["isRepresented"]
9
+ @display_name = json["displayName"]
10
+ @identifier = json["identifier"]
11
+ end
12
+ end
13
+
14
+ class PercentilesService
15
+ def initialize(bundle_id, json)
16
+ @json = json
17
+ @bundle_id = bundle_id
18
+ end
19
+
20
+ def percentiles
21
+ percentiles_json = @json["filterCriteriaSets"]["percentiles"]
22
+ percentiles_json.map { |percentile_json| Percentile.new percentile_json }
23
+ end
24
+
25
+ def structure
26
+ structure = XcMetricsAggregator::TableStructure.new
27
+ structure.title = @bundle_id
28
+ structure.headings = headings()
29
+ structure.rows = rows()
30
+ structure
31
+ end
32
+
33
+ def rows
34
+ percentiles.map { |percentile| [percentile.display_name, percentile.identifier] }
35
+ end
36
+
37
+ def headings
38
+ ["percentile", "id"]
39
+ end
40
+
41
+ def get_percentile(identifier)
42
+ if identifier.nil?
43
+ nil
44
+ end
45
+
46
+
47
+ res = percentiles.find do |percentile|
48
+ percentile.identifier == identifier
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,17 @@
1
+ require 'xc_metrics_aggregator/formatter/formatter'
2
+
3
+ module XcMetricsAggregator
4
+ class Structure
5
+ def format(formatter)
6
+ formatter.format(self)
7
+ end
8
+ end
9
+
10
+ class TableStructure < Structure
11
+ attr_accessor :title, :headings, :rows
12
+ end
13
+
14
+ class ChartStructure < Structure
15
+ attr_accessor :series, :samples, :unit, :dscription
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module XcMetricsAggregator
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,125 @@
1
+ require "xc_metrics_aggregator/version"
2
+ require 'xc_metrics_aggregator/product'
3
+ require 'xc_metrics_aggregator/crawler'
4
+ require 'xc_metrics_aggregator/service/devices_service'
5
+ require 'xc_metrics_aggregator/service/percentiles_service'
6
+ require 'xc_metrics_aggregator/service/categories_service'
7
+ require 'xc_metrics_aggregator/service/metrics_service'
8
+ require 'xc_metrics_aggregator/service/latest_service'
9
+ require 'xc_metrics_aggregator/formatter/formatter'
10
+ require 'xc_metrics_aggregator/formatter/output_format'
11
+ require 'thor'
12
+ require 'ascii_charts'
13
+ require 'pp'
14
+
15
+ module XcMetricsAggregator
16
+
17
+ class Error < StandardError; end
18
+ class CLI < Thor
19
+
20
+ desc "crowl", "Aggregate raw data of Xcode Organizer Metrics"
21
+ def crowl
22
+ Crawler.execute()
23
+ end
24
+
25
+ desc "apps", "Shows available bundle ids on Xcode Organizer Metrics"
26
+ option :available_path, type: :boolean, aliases: "-a", default: false
27
+ option :format, aliases: "-f", type: :string, default: "ascii"
28
+ def apps
29
+ service = ProductsService.new
30
+ puts service.structure(options[:available_path]).format Formatter.get_formatter(format)
31
+ end
32
+
33
+ option :bundle_ids, :aliases => "-b", type: :array, default: []
34
+ option :format, :aliases => "-f", type: :string, default: "ascii"
35
+ desc "devices [-b <bundle id 1> <bundle id 2> ...] [-f <format>]", "Show available devices by builde id"
36
+ def devices
37
+ each_product do |product|
38
+ product.try_to_open do |json|
39
+ puts DevicesService
40
+ .new(product.bundle_id, json)
41
+ .structure
42
+ .format Formatter.get_formatter(format)
43
+ puts "\n\n"
44
+ end
45
+ end
46
+ end
47
+
48
+ option :bundle_ids, :aliases => "-b", type: :array, default: []
49
+ option :format, :aliases => "-f", type: :string, default: "ascii"
50
+ desc "percentiles [-b <bundle id>,...] [-f <format>]", "Show available percentiles by builde id"
51
+ def percentiles
52
+ each_product do |product|
53
+ product.try_to_open do |json|
54
+ puts PercentilesService
55
+ .new(product.bundle_id, json)
56
+ .structure
57
+ .format Formatter.get_formatter(format)
58
+ puts "\n\n"
59
+ end
60
+ end
61
+ end
62
+
63
+
64
+ option :bundle_id, :aliases => "-b", require: true
65
+ option :format, :aliases => "-f", type: :string, default: "ascii"
66
+ desc "categories -b <bundle id> [-f <format>]", "Show available categories by builde id"
67
+ def sections
68
+ product.try_to_open do |json|
69
+ puts CategoriesService
70
+ .new(product.bundle_id, json)
71
+ .structure
72
+ .format Formatter.get_formatter(format)
73
+ end
74
+ end
75
+
76
+ option :section, :aliases => "-s", require: true
77
+ option :bundle_id, :aliases => "-b", require: true
78
+ option :format, :aliases => "-f", type: :string, default: "ascii"
79
+ option :version, :aliases => "-v", require: false
80
+ option :device, :aliases => "-d", require: false
81
+ option :percentile, :aliases => "-p", require: false
82
+ desc "metrics -b <bundle id> -s <section> [-f <format>] [-d <device id>] [-p <percentile id>] [-v <version>]", "Show metrics data to a builde id"
83
+ def metrics
84
+ product.try_to_open do |json|
85
+ structure = MetricsService
86
+ .new(product.bundle_id, json)
87
+ .structures(options[:section], options[:device], options[:percentile], options[:version])
88
+
89
+ puts structure.format Formatter.get_formatter(format)
90
+ puts "\n\n"
91
+ end
92
+ end
93
+
94
+ option :section, :aliases => "-s", require: true
95
+ option :device, :aliases => "-d", type: :string
96
+ option :percentile, :aliases => "-p", type: :string
97
+ option :format, :aliases => "-f", type: :string, default: "ascii"
98
+ desc "latest -s <section> [-p <percentile id>] [-d <device id>] [-f <format>]", "Compare a latest version's metrics between available builde ids"
99
+ def latest
100
+ deviceid = options[:device]
101
+ percentileid = options[:percentile]
102
+ section = options[:section]
103
+
104
+ puts LatestService
105
+ .new(section, deviceid, percentileid)
106
+ .structure
107
+ .format Formatter.get_formatter(format)
108
+ end
109
+
110
+ private
111
+ def format
112
+ OutputFormat.all.find { |v| v == options[:format] }
113
+ end
114
+
115
+ def each_product
116
+ ProductsService.new.each_product(options[:bundle_ids]) { |product|
117
+ yield product
118
+ }
119
+ end
120
+
121
+ def product
122
+ ProductsService.new.target options[:bundle_id]
123
+ end
124
+ end
125
+ end