firebase-stats 1.0.3 → 1.0.7
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.
- checksums.yaml +4 -4
- data/bin/firebase-stats +50 -28
- data/lib/device_utils.rb +24 -0
- data/lib/firebase-stats.rb +2 -0
- data/lib/reader.rb +47 -19
- data/lib/section_not_found_error.rb +9 -0
- data/lib/version.rb +2 -2
- data/lib/wrapper.rb +90 -74
- metadata +5 -4
- data/lib/.DS_Store +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f242227d13ffe84bca59bc31022c4d8628e8df5a7685d5ae1d6dd97c1aa937c
|
4
|
+
data.tar.gz: 06e6280fdcab4baac6818eb42f1adb9c7a4153e81620557a0da8115b4f21c7bf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3e663d068be4f12c0c3ea98fd7aeda11184550a0ca3732effe19a1999733fab5fa960c14a6dfbd5f55ba6549908d8665f34d93917d1768b101dff50f69b383b
|
7
|
+
data.tar.gz: ef547e9e6b067dba32befac728beb80eeccd1f9217b4641cc358f1f72765eff5bfba90ad79dde97fdc7a09b5d97ff42ae6de3be00164ea8de759ec85d2e6f673
|
data/bin/firebase-stats
CHANGED
@@ -29,13 +29,17 @@ command :devices do |c|
|
|
29
29
|
c.option '--platform STRING', String, 'Show only stats for this OS. Either ios, android or all (default)'
|
30
30
|
|
31
31
|
c.action do |args, options|
|
32
|
-
|
33
|
-
|
32
|
+
begin
|
33
|
+
stats = FirebaseStats::Reader.new
|
34
|
+
stats.parse_file(args[0])
|
34
35
|
|
35
|
-
|
36
|
+
platform = map_platform(options)
|
36
37
|
|
37
|
-
|
38
|
-
|
38
|
+
wrapper = FirebaseStats::Wrapper.new(stats)
|
39
|
+
tp wrapper.devices(friendly: options.friendly, limit: options.limit, platform: platform)
|
40
|
+
rescue FirebaseStats::SectionNotFoundError => error
|
41
|
+
print_data_error(error)
|
42
|
+
end
|
39
43
|
end
|
40
44
|
end
|
41
45
|
|
@@ -48,21 +52,23 @@ command :os do |c|
|
|
48
52
|
c.option '--platform STRING', String, 'Show only stats for this OS. Either ios, android or all (default)'
|
49
53
|
|
50
54
|
c.action do |args, options|
|
51
|
-
|
52
|
-
|
55
|
+
begin
|
56
|
+
stats = FirebaseStats::Reader.new
|
57
|
+
stats.parse_file(args[0])
|
58
|
+
|
59
|
+
platform = map_platform(options)
|
53
60
|
|
54
|
-
|
61
|
+
wrapper = FirebaseStats::Wrapper.new(stats)
|
55
62
|
|
56
|
-
|
63
|
+
grouped = options.grouped || false
|
64
|
+
major_order = options.version_sorted || false
|
57
65
|
|
58
|
-
|
66
|
+
data = wrapper.os(platform: platform, grouped: grouped, major_order: major_order)
|
59
67
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
end.reverse
|
68
|
+
tp data
|
69
|
+
rescue FirebaseStats::SectionNotFoundError => error
|
70
|
+
print_data_error(error)
|
64
71
|
end
|
65
|
-
tp data
|
66
72
|
end
|
67
73
|
end
|
68
74
|
|
@@ -72,13 +78,15 @@ command :gender do |c|
|
|
72
78
|
c.description = 'Prints out a table with number of users of each gender'
|
73
79
|
|
74
80
|
c.action do |args, options|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
81
|
+
begin
|
82
|
+
stats = FirebaseStats::Reader.new
|
83
|
+
stats.parse_file(args[0])
|
84
|
+
|
85
|
+
wrapper = FirebaseStats::Wrapper.new(stats)
|
86
|
+
tp wrapper.gender
|
87
|
+
rescue FirebaseStats::SectionNotFoundError => error
|
88
|
+
print_data_error(error)
|
89
|
+
end
|
82
90
|
end
|
83
91
|
end
|
84
92
|
|
@@ -88,12 +96,26 @@ command :gender_age do |c|
|
|
88
96
|
c.description = 'Prints out a table with percentage of users of each gender grouped by age'
|
89
97
|
|
90
98
|
c.action do |args, options|
|
91
|
-
|
92
|
-
|
99
|
+
begin
|
100
|
+
stats = FirebaseStats::Reader.new
|
101
|
+
stats.parse_file(args[0])
|
102
|
+
|
103
|
+
wrapper = FirebaseStats::Wrapper.new(stats)
|
104
|
+
tp wrapper.gender_age
|
105
|
+
rescue FirebaseStats::SectionNotFoundError => error
|
106
|
+
print_data_error(error)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
93
110
|
|
94
|
-
|
111
|
+
private
|
95
112
|
|
96
|
-
|
97
|
-
|
98
|
-
|
113
|
+
|
114
|
+
# @param [SectionNotFoundError] error
|
115
|
+
def print_data_error(error)
|
116
|
+
tip = FirebaseStats::Wrapper.tip(error.section)
|
117
|
+
expected_header = FirebaseStats::Reader.search_string(error.section)
|
118
|
+
puts "Unable to find that specific data in the input file"
|
119
|
+
puts "For the data you requested, the following CSV header should be contained within the file: '#{expected_header}'"
|
120
|
+
puts tip unless tip.nil?
|
99
121
|
end
|
data/lib/device_utils.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module FirebaseStats
|
2
|
+
# Parses the Firebase CSV file into sections
|
3
|
+
class DeviceUtils
|
4
|
+
# Is this device name an iOS device?
|
5
|
+
# @param [CSV::Row] device_name
|
6
|
+
def self.ios_device?(device_name)
|
7
|
+
device_name.downcase.include?('iphone') or device_name.downcase.include?('ipad') or device_name.downcase.include?('ipod')
|
8
|
+
end
|
9
|
+
|
10
|
+
# Filters a device list to only the requested platform
|
11
|
+
# @param [CSV::Table] device_data
|
12
|
+
# @param [Symbol] platform One of :all, :ios, :android
|
13
|
+
def self.filter_device(device_data, platform)
|
14
|
+
case platform
|
15
|
+
when :android
|
16
|
+
device_data.reject { |row| ios_device? row['Device model'] }
|
17
|
+
when :ios
|
18
|
+
device_data.select { |row| ios_device? row['Device model'] }
|
19
|
+
else
|
20
|
+
device_data
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/firebase-stats.rb
CHANGED
data/lib/reader.rb
CHANGED
@@ -1,17 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module FirebaseStats
|
4
4
|
require 'csv'
|
5
5
|
|
6
6
|
# Parses the Firebase CSV file into sections
|
7
7
|
class Reader
|
8
|
-
attr_reader :data
|
9
|
-
|
10
8
|
def initialize
|
11
9
|
super
|
12
10
|
@data = {}
|
13
11
|
end
|
14
12
|
|
13
|
+
def num_sections
|
14
|
+
@data.length
|
15
|
+
end
|
16
|
+
|
17
|
+
def get(section)
|
18
|
+
found = @data[section]
|
19
|
+
raise SectionNotFoundError.new(section) if found.nil?
|
20
|
+
found
|
21
|
+
end
|
22
|
+
|
15
23
|
# @param [String] filename
|
16
24
|
def parse_file(filename)
|
17
25
|
lines = File.readlines(filename)
|
@@ -20,6 +28,7 @@ class FirebaseStats
|
|
20
28
|
|
21
29
|
# @param [Array<String>] input
|
22
30
|
def parse(input)
|
31
|
+
@data = {}
|
23
32
|
curr_lines = []
|
24
33
|
input.each_with_index do |line, idx|
|
25
34
|
curr_lines.push(line) unless comment?(line) || line.strip.empty?
|
@@ -31,22 +40,15 @@ class FirebaseStats
|
|
31
40
|
end
|
32
41
|
end
|
33
42
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
return if section.nil?
|
40
|
-
|
41
|
-
parsed = CSV.parse(lines.join, headers: true)
|
42
|
-
@data[section] = parsed if @data[section].nil?
|
43
|
+
# Get the string that is used to find a given section in the CSV
|
44
|
+
def self.search_string(section)
|
45
|
+
header = Reader.mappings.key(section)
|
46
|
+
header = "Category,Male,Other,Female" if header.nil? and section == :gender_age
|
47
|
+
return header
|
43
48
|
end
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
# rubocop:disable Metrics/MethodLength
|
48
|
-
def match_header(header)
|
49
|
-
mappings = {
|
50
|
+
def self.mappings
|
51
|
+
{
|
50
52
|
'Day,28-Day,7-Day,1-Day' => :active_users,
|
51
53
|
'Day,Average engagement time' => :daily_engagement,
|
52
54
|
'Page path and screen class,User engagement,Screen views' => :screens,
|
@@ -59,16 +61,42 @@ class FirebaseStats
|
|
59
61
|
'Device model,Users' => :devices,
|
60
62
|
'OS with version,Users' => :os_version,
|
61
63
|
'Gender,Users' => :gender,
|
62
|
-
'Category,Female,Male' => :gender_age,
|
63
64
|
'Platform,Users' => :platform,
|
64
65
|
'Platform,Users,% Total,User engagement,Total revenue' => :platform_engagement
|
65
66
|
}
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# @param [Array<String>] lines
|
72
|
+
def process_lines(lines)
|
73
|
+
section = match_header lines[0]
|
74
|
+
return if section.nil?
|
75
|
+
|
76
|
+
parsed = CSV.parse(lines.join, headers: true)
|
77
|
+
@data[section] = parsed if @data[section].nil?
|
78
|
+
end
|
79
|
+
|
80
|
+
# Maps a given CSV header to a section
|
81
|
+
# @param [String] header The CSV header line to parse
|
82
|
+
# @return [Symbol, nil] The section, or nil if not found
|
83
|
+
# rubocop:disable Metrics/MethodLength
|
84
|
+
def match_header(header)
|
85
|
+
# All of the section headers that can be found in the CSV file, mapping to our internal section symbols
|
86
|
+
|
87
|
+
cleaned_header = header.strip
|
88
|
+
section = Reader.mappings[cleaned_header]
|
66
89
|
|
67
|
-
|
90
|
+
# Kludge for gender_age parsing as the headers aren't always in the right order, so rule out
|
91
|
+
# all the other sections first
|
92
|
+
section = :gender_age if section.nil? and cleaned_header.include? 'Category,'
|
93
|
+
section
|
68
94
|
end
|
69
95
|
# rubocop:enable Metrics/MethodLength
|
70
96
|
|
97
|
+
# Is this line a comment
|
71
98
|
# @param [String] line
|
99
|
+
# @return [Boolean]
|
72
100
|
def comment?(line)
|
73
101
|
line.include?('#')
|
74
102
|
end
|
data/lib/version.rb
CHANGED
data/lib/wrapper.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module FirebaseStats
|
4
4
|
require 'csv'
|
5
5
|
require 'android/devices'
|
6
6
|
require 'open-uri'
|
@@ -8,65 +8,41 @@ class FirebaseStats
|
|
8
8
|
# Transforms the parsed Firebase file into something more user friendly
|
9
9
|
class Wrapper
|
10
10
|
# @param [FirebaseStats::Reader] stats
|
11
|
-
|
12
|
-
def initialize(stats, platform = :all)
|
11
|
+
def initialize(stats)
|
13
12
|
super()
|
14
13
|
@stats = stats
|
15
|
-
@platform = platform
|
16
|
-
end
|
17
|
-
|
18
|
-
def os_version
|
19
|
-
filtered = filter_os(@stats.data[:os_version], @platform)
|
20
|
-
|
21
|
-
cleaned = []
|
22
|
-
filtered.each do |row|
|
23
|
-
cleaned << {
|
24
|
-
'version' => row['OS with version'],
|
25
|
-
'count' => row['Users'].to_i,
|
26
|
-
'percentage' => as_percentage(os_total, row['Users'].to_f)
|
27
|
-
}
|
28
|
-
end
|
29
|
-
cleaned
|
30
|
-
end
|
31
|
-
|
32
|
-
def os_grouped
|
33
|
-
raw_os = @stats.data[:os_version]
|
34
|
-
|
35
|
-
grouped = case @platform
|
36
|
-
when :ios
|
37
|
-
ios_os_group(raw_os)
|
38
|
-
when :android
|
39
|
-
android_os_group(raw_os)
|
40
|
-
else
|
41
|
-
android_os_group(raw_os).merge ios_os_group(raw_os)
|
42
|
-
end
|
43
|
-
computed = []
|
44
|
-
grouped.each do |k, v|
|
45
|
-
version_name = k
|
46
|
-
total = v.map { |version| version['Users'].to_i }.reduce(0, :+)
|
47
|
-
computed << { 'version' => version_name, 'total' => total, 'percentage' => as_percentage(os_total, total.to_f) }
|
48
|
-
end
|
49
|
-
computed
|
50
14
|
end
|
51
15
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
16
|
+
# Get all OS versions, grouped by Major version
|
17
|
+
# @param [Symbol] platform One of :all, :ios, :android
|
18
|
+
# @param [Boolean] grouped Group by Major OS version
|
19
|
+
# @param [Boolean] major_order Order by Major OS version (instead of percentage)
|
20
|
+
def os(platform: :all, grouped: true, major_order: true)
|
21
|
+
os_data = all_os
|
22
|
+
filtered = filter_os(os_data, platform)
|
23
|
+
|
24
|
+
data = if grouped
|
25
|
+
make_group_stats(filtered, platform)
|
26
|
+
else
|
27
|
+
filtered
|
28
|
+
end
|
29
|
+
|
30
|
+
major_order ? major_version_sort(data) : data
|
59
31
|
end
|
60
32
|
|
61
|
-
|
62
|
-
|
33
|
+
# Gets all devices
|
34
|
+
# @param [Boolean] friendly Transform the Android model numbers into their human numaes
|
35
|
+
# @param [Integer] limit Number of devices to turn
|
36
|
+
# @param [Symbol] platform One of :all, :ios, :android
|
37
|
+
def devices(friendly: false, limit: 10, platform: :all)
|
38
|
+
filtered = DeviceUtils.filter_device(@stats.get(:devices), platform)
|
63
39
|
filtered = filtered.take(limit || 10)
|
64
40
|
cleaned = []
|
65
41
|
filtered.each do |row|
|
66
42
|
device = {
|
67
43
|
'model' => row['Device model']
|
68
44
|
}
|
69
|
-
if friendly && ((
|
45
|
+
if friendly && ((platform == :all) || (platform == :android))
|
70
46
|
mapped = Android::Devices.search_by_model(row['Device model'])
|
71
47
|
device['friendly'] = if mapped.nil?
|
72
48
|
row['Device model']
|
@@ -82,25 +58,26 @@ class FirebaseStats
|
|
82
58
|
end
|
83
59
|
|
84
60
|
def gender
|
85
|
-
raw = @stats.
|
61
|
+
raw = @stats.get(:gender)
|
86
62
|
data = []
|
87
63
|
raw.each do |row|
|
88
64
|
data << {
|
89
65
|
'gender' => row['Gender'],
|
90
|
-
'count' => row['Users']
|
66
|
+
'count' => row['Users'].to_i
|
91
67
|
}
|
92
68
|
end
|
93
69
|
data
|
94
70
|
end
|
95
71
|
|
96
72
|
def gender_age
|
97
|
-
raw = @stats.
|
73
|
+
raw = @stats.get(:gender_age)
|
98
74
|
data = []
|
99
75
|
raw.each do |row|
|
100
76
|
data << {
|
101
77
|
'age' => row['Category'],
|
102
78
|
'male' => (row['Male'].to_f * 100).round(2),
|
103
|
-
'female' => (row['Female'].to_f * 100).round(2)
|
79
|
+
'female' => (row['Female'].to_f * 100).round(2),
|
80
|
+
'other' => (row['Other'].to_f * 100).round(2)
|
104
81
|
}
|
105
82
|
end
|
106
83
|
data
|
@@ -113,32 +90,14 @@ class FirebaseStats
|
|
113
90
|
def filter_os(os_data, platform)
|
114
91
|
case platform
|
115
92
|
when :android
|
116
|
-
os_data.select { |row| row['
|
93
|
+
os_data.select { |row| row['version'].downcase.include?('android') }
|
117
94
|
when :ios
|
118
|
-
os_data.select { |row| row['
|
95
|
+
os_data.select { |row| row['version'].downcase.include?('ios') }
|
119
96
|
else
|
120
97
|
os_data
|
121
98
|
end
|
122
99
|
end
|
123
100
|
|
124
|
-
# @param [CSV::Table] device_data
|
125
|
-
# @param [Symbol] platform One of :all, :ios, :android
|
126
|
-
def filter_device(device_data, platform)
|
127
|
-
case platform
|
128
|
-
when :android
|
129
|
-
device_data.reject { |row| ios_device? row['Device model'] }
|
130
|
-
when :ios
|
131
|
-
device_data.select { |row| ios_device? row['Device model'] }
|
132
|
-
else
|
133
|
-
device_data
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
# @param [CSV::Row] device_name
|
138
|
-
def ios_device?(device_name)
|
139
|
-
device_name.downcase.include?('iphone') or device_name.downcase.include?('ipad') or device_name.downcase.include?('ipod')
|
140
|
-
end
|
141
|
-
|
142
101
|
def as_percentage(total, value)
|
143
102
|
percentage = (value / total) * 100
|
144
103
|
if percentage < 0.01
|
@@ -149,11 +108,68 @@ class FirebaseStats
|
|
149
108
|
end
|
150
109
|
|
151
110
|
def ios_os_group(os_details)
|
152
|
-
filter_os(os_details, :ios).group_by { |row| row['
|
111
|
+
filter_os(os_details, :ios).group_by { |row| row['version'].match('(iOS [0-9]{1,2})').captures[0] }
|
153
112
|
end
|
154
113
|
|
155
114
|
def android_os_group(os_details)
|
156
|
-
filter_os(os_details, :android).group_by { |row| row['
|
115
|
+
filter_os(os_details, :android).group_by { |row| row['version'].match('(Android [0-9]{1,2})').captures[0] }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get all OS versions
|
119
|
+
def all_os
|
120
|
+
data = @stats.get(:os_version)
|
121
|
+
|
122
|
+
data.map do |row|
|
123
|
+
{
|
124
|
+
'version' => row['OS with version'],
|
125
|
+
'count' => row['Users'].to_i
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def make_group_stats(os_data, platform)
|
131
|
+
data = make_os_groups(os_data, platform)
|
132
|
+
|
133
|
+
total_devices = os_total(os_data)
|
134
|
+
data.map do |k, v|
|
135
|
+
version_name = k
|
136
|
+
group_total = v.map { |version| version['count'].to_i }.reduce(0, :+)
|
137
|
+
{ 'version' => version_name,
|
138
|
+
'total' => group_total,
|
139
|
+
'percentage' => as_percentage(total_devices.to_f, group_total) }
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def make_os_groups(os_data, platform)
|
144
|
+
case platform
|
145
|
+
when :ios
|
146
|
+
ios_os_group(os_data)
|
147
|
+
when :android
|
148
|
+
android_os_group(os_data)
|
149
|
+
else
|
150
|
+
android_os_group(os_data).merge ios_os_group(os_data)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def os_total(os_data)
|
155
|
+
os_data.map { |row| row['count'] }.reduce(0, :+)
|
156
|
+
end
|
157
|
+
|
158
|
+
def major_version_sort(data)
|
159
|
+
data.sort_by do |row|
|
160
|
+
version = row['version']
|
161
|
+
number = version.match('([0-9.]+)').captures[0]
|
162
|
+
Gem::Version.new(number)
|
163
|
+
end.reverse
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.tip(section)
|
167
|
+
tips = {
|
168
|
+
:os_version => "This data can now be found in the Audiences section of Firebase Analytics. Before you export the CSV file, change one of the charts to `Users` by `OS with version`",
|
169
|
+
:gender_age => "Note: The columns for Gender+Age are not always in the same order, but this is taken into account when searching",
|
170
|
+
:devices => "Note: If you export from the Tech Details: Device Model page, this is currently unsupported as it has two different headers. Use the export from the main Dashboard page"
|
171
|
+
}
|
172
|
+
tips[section]
|
157
173
|
end
|
158
174
|
end
|
159
175
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: firebase-stats
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- chedabob
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-01-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: android-devices
|
@@ -103,9 +103,10 @@ extensions: []
|
|
103
103
|
extra_rdoc_files: []
|
104
104
|
files:
|
105
105
|
- bin/firebase-stats
|
106
|
-
- lib
|
106
|
+
- lib/device_utils.rb
|
107
107
|
- lib/firebase-stats.rb
|
108
108
|
- lib/reader.rb
|
109
|
+
- lib/section_not_found_error.rb
|
109
110
|
- lib/version.rb
|
110
111
|
- lib/wrapper.rb
|
111
112
|
homepage: https://github.com/chedabob/firebase-stats
|
@@ -127,7 +128,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
127
128
|
- !ruby/object:Gem::Version
|
128
129
|
version: '0'
|
129
130
|
requirements: []
|
130
|
-
rubygems_version: 3.
|
131
|
+
rubygems_version: 3.0.9
|
131
132
|
signing_key:
|
132
133
|
specification_version: 4
|
133
134
|
summary: Firebase CSV Stats CLI
|
data/lib/.DS_Store
DELETED
Binary file
|