xc_metrics_aggregator 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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