evva 0.8.5 → 0.9.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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +5 -0
- data/Gemfile.lock +1 -1
- data/README.md +39 -0
- data/changelog.md +4 -0
- data/evva.gemspec +1 -1
- data/lib/evva/analytics_event.rb +11 -3
- data/lib/evva/config.rb +6 -1
- data/lib/evva/google_sheet.rb +27 -5
- data/lib/evva/version.rb +2 -2
- data/lib/evva.rb +35 -0
- data/spec/evva_spec.rb +183 -0
- data/spec/fixtures/sample_public_enums_with_platform.csv +6 -0
- data/spec/fixtures/sample_public_events_with_platform.csv +7 -0
- data/spec/lib/evva/config_spec.rb +18 -0
- data/spec/lib/evva/google_sheet_spec.rb +110 -4
- data/spec/platform_filter_spec.rb +147 -0
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a759833afbdc6f48b186bce87555b4fae3da30812de523ddae04468244a7aee9
|
|
4
|
+
data.tar.gz: 421625130a0be7491b0e49ea5b6cdb434b7cb3a6f7c52d20adf02ff6f11ca205
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9bd2cc069ab15eed7a02c6ed444f22579236446a7a3b7b9c1772df26deefaf442dd568334ab0903059f056a65fc1a7ceadb3e6fd6df1975a6d097ab7bffeca8
|
|
7
|
+
data.tar.gz: d8f1f9574a97f4060924514d99102abcdad78d71d75cd0a378bb94423f1cd359de171908fe11c8579370035ddab2a31eb0e429e8a27b27aef1b56cd12ba9de92
|
data/.rubocop_todo.yml
CHANGED
|
@@ -109,6 +109,7 @@ Layout/SpaceInsideHashLiteralBraces:
|
|
|
109
109
|
Exclude:
|
|
110
110
|
- 'lib/evva/config.rb'
|
|
111
111
|
- 'lib/evva/logger.rb'
|
|
112
|
+
- 'spec/evva_spec.rb'
|
|
112
113
|
- 'spec/lib/evva/config_spec.rb'
|
|
113
114
|
- 'spec/lib/evva/google_sheet_spec.rb'
|
|
114
115
|
- 'spec/lib/evva/kotlin_generator_spec.rb'
|
|
@@ -253,6 +254,8 @@ Style/HashSyntax:
|
|
|
253
254
|
- 'Gemfile'
|
|
254
255
|
- 'Rakefile'
|
|
255
256
|
- 'lib/evva.rb'
|
|
257
|
+
- 'spec/evva_spec.rb'
|
|
258
|
+
- 'spec/platform_filter_spec.rb'
|
|
256
259
|
- 'lib/evva/config.rb'
|
|
257
260
|
- 'lib/evva/google_sheet.rb'
|
|
258
261
|
- 'lib/evva/kotlin_generator.rb'
|
|
@@ -359,6 +362,7 @@ Style/SymbolProc:
|
|
|
359
362
|
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
|
360
363
|
Style/TrailingCommaInArrayLiteral:
|
|
361
364
|
Exclude:
|
|
365
|
+
- 'spec/evva_spec.rb'
|
|
362
366
|
- 'spec/lib/evva/google_sheet_spec.rb'
|
|
363
367
|
- 'spec/lib/evva/kotlin_generator_spec.rb'
|
|
364
368
|
- 'spec/lib/evva/swift_generator_spec.rb'
|
|
@@ -370,6 +374,7 @@ Style/TrailingCommaInArrayLiteral:
|
|
|
370
374
|
Style/TrailingCommaInHashLiteral:
|
|
371
375
|
Exclude:
|
|
372
376
|
- 'lib/evva/config.rb'
|
|
377
|
+
- 'spec/evva_spec.rb'
|
|
373
378
|
- 'lib/evva/kotlin_generator.rb'
|
|
374
379
|
- 'lib/evva/swift_generator.rb'
|
|
375
380
|
- 'spec/lib/evva/config_spec.rb'
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -36,4 +36,43 @@ Evva automatically generates code for triggering events based on a Google Sheets
|
|
|
36
36
|
destinations_file_name: /file/with/destinations
|
|
37
37
|
special_enum_file_name: /file/with/special/enum/properties/
|
|
38
38
|
swift_public: false # optional; when true (iOS), generated Swift uses the public access modifier for generated extensions
|
|
39
|
+
filter_by_platforms: # optional; array of platforms to keep (e.g. [ios])
|
|
40
|
+
- ios
|
|
39
41
|
```
|
|
42
|
+
|
|
43
|
+
## The events sheet
|
|
44
|
+
|
|
45
|
+
The events sheet is read by column header, so column order does not matter and
|
|
46
|
+
unknown columns are ignored.
|
|
47
|
+
|
|
48
|
+
| Column | Required | Meaning |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| `Event Name` | yes | The event name |
|
|
51
|
+
| `Event Properties` | no | Comma separated `name:Type` pairs |
|
|
52
|
+
| `Event Destination` | no | Comma separated destinations |
|
|
53
|
+
| `Platform` | no | Which platforms track the event |
|
|
54
|
+
|
|
55
|
+
### Platform
|
|
56
|
+
|
|
57
|
+
`Platform` tags an event with the platforms it belongs to. Filtering is driven
|
|
58
|
+
by the `filter_by_platforms` key in `evva_config.yml`: only events matching one
|
|
59
|
+
of the listed platforms (or unrestricted events) are generated. When the key is
|
|
60
|
+
absent, every event is generated.
|
|
61
|
+
|
|
62
|
+
Any value is accepted as a platform name (case insensitive, comma separated).
|
|
63
|
+
An empty cell means the event is unrestricted and will never be filtered out.
|
|
64
|
+
|
|
65
|
+
The header is matched ignoring case and surrounding whitespace, so `platform` and
|
|
66
|
+
`Platform ` are both found.
|
|
67
|
+
|
|
68
|
+
When no `Platform` column is found the run says so, because "every event was
|
|
69
|
+
generated" is otherwise indistinguishable from a header that failed to match:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
[INFO] No Platform column in the events sheet, every event will be generated for every platform
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Enums whose only referencing events were filtered out are pruned, unless a
|
|
76
|
+
people property also references them.
|
|
77
|
+
|
|
78
|
+
People properties have no `Platform` column and are never filtered.
|
data/changelog.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.9.0] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
- Add `Platform` column support in the events sheet and `filter_by_platforms` configuration to filter generated events by platform
|
|
8
|
+
|
|
5
9
|
## [0.8.5] - 2026-08-21
|
|
6
10
|
|
|
7
11
|
- Fix `native_type` replacing `Long`/`Boolean` substrings inside custom type names (e.g. `FlyoverLongPressAction` → `FlyoverIntPressAction`)
|
data/evva.gemspec
CHANGED
|
@@ -19,6 +19,6 @@ Gem::Specification.new do |s|
|
|
|
19
19
|
s.required_ruby_version = ">= 3.2.0"
|
|
20
20
|
|
|
21
21
|
s.add_runtime_dependency "colorize", "~> 1.0"
|
|
22
|
-
s.add_runtime_dependency "csv", "
|
|
22
|
+
s.add_runtime_dependency "csv", "~> 3.0"
|
|
23
23
|
s.add_runtime_dependency "safe_yaml", "~> 1.0"
|
|
24
24
|
end
|
data/lib/evva/analytics_event.rb
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
module Evva
|
|
2
2
|
class AnalyticsEvent
|
|
3
|
-
attr_reader :event_name, :properties, :destinations
|
|
3
|
+
attr_reader :event_name, :properties, :destinations, :platforms
|
|
4
4
|
|
|
5
|
-
def initialize(event_name, properties, destinations)
|
|
5
|
+
def initialize(event_name, properties, destinations, platforms = nil)
|
|
6
6
|
@event_name = event_name
|
|
7
7
|
@properties = properties
|
|
8
8
|
@destinations = destinations
|
|
9
|
+
@platforms = platforms
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def supports_platform?(platform)
|
|
13
|
+
return true if platforms.nil?
|
|
14
|
+
|
|
15
|
+
platforms.include?(platform.to_s.downcase)
|
|
9
16
|
end
|
|
10
17
|
|
|
11
18
|
def ==(other)
|
|
12
19
|
event_name == other.event_name &&
|
|
13
20
|
properties == other.properties &&
|
|
14
|
-
destinations == other.destinations
|
|
21
|
+
destinations == other.destinations &&
|
|
22
|
+
platforms == other.platforms
|
|
15
23
|
end
|
|
16
24
|
end
|
|
17
25
|
end
|
data/lib/evva/config.rb
CHANGED
|
@@ -68,6 +68,10 @@ module Evva
|
|
|
68
68
|
@hash[:exclude_destinations] || []
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
def filter_by_platforms
|
|
72
|
+
@hash[:filter_by_platforms]
|
|
73
|
+
end
|
|
74
|
+
|
|
71
75
|
CONFIG_STRUCT = {
|
|
72
76
|
type: Hash,
|
|
73
77
|
elements: {
|
|
@@ -83,7 +87,8 @@ module Evva
|
|
|
83
87
|
destinations_file_name: { type: String },
|
|
84
88
|
package_name: { type: String },
|
|
85
89
|
swift_public: { type: Object, optional: true },
|
|
86
|
-
exclude_destinations: { type: Array, optional: true }
|
|
90
|
+
exclude_destinations: { type: Array, optional: true },
|
|
91
|
+
filter_by_platforms: { type: Array, optional: true }
|
|
87
92
|
}
|
|
88
93
|
}.freeze
|
|
89
94
|
|
data/lib/evva/google_sheet.rb
CHANGED
|
@@ -6,6 +6,7 @@ module Evva
|
|
|
6
6
|
EVENT_NAME = "Event Name"
|
|
7
7
|
EVENT_PROPERTIES = "Event Properties"
|
|
8
8
|
EVENT_DESTINATION = "Event Destination"
|
|
9
|
+
EVENT_PLATFORM = "Platform"
|
|
9
10
|
|
|
10
11
|
PROPERTY_NAME = "Property Name"
|
|
11
12
|
PROPERTY_TYPE = "Property Type"
|
|
@@ -26,11 +27,17 @@ module Evva
|
|
|
26
27
|
get_csv(@events_url)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
@events ||=
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
@events ||= begin
|
|
31
|
+
platform_header = header_matching(@events_csv, EVENT_PLATFORM)
|
|
32
|
+
Logger.info("No #{EVENT_PLATFORM} column in the events sheet, every event will be generated for every platform") if platform_header.nil?
|
|
33
|
+
|
|
34
|
+
@events_csv.map do |row|
|
|
35
|
+
event_name = row[EVENT_NAME]
|
|
36
|
+
properties = hash_parser(row[EVENT_PROPERTIES])
|
|
37
|
+
destinations = row[EVENT_DESTINATION]&.split(",")
|
|
38
|
+
platforms = platform_parser(platform_header && row[platform_header])
|
|
39
|
+
Evva::AnalyticsEvent.new(event_name, properties, destinations || [], platforms)
|
|
40
|
+
end
|
|
34
41
|
end
|
|
35
42
|
end
|
|
36
43
|
|
|
@@ -96,6 +103,21 @@ module Evva
|
|
|
96
103
|
response.body
|
|
97
104
|
end
|
|
98
105
|
|
|
106
|
+
# Matched ignoring case and surrounding whitespace, because a header of
|
|
107
|
+
# "platform" or "Platform " means what it plainly means.
|
|
108
|
+
def header_matching(csv, name)
|
|
109
|
+
csv.headers.compact.find { |header| header.to_s.strip.casecmp?(name) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# nil (no Platform column or empty cell) means unrestricted (never filtered out).
|
|
113
|
+
# Concrete platform tokens are stored lowercased and deduplicated.
|
|
114
|
+
def platform_parser(platform_list)
|
|
115
|
+
tokens = platform_list.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
116
|
+
return nil if tokens.empty?
|
|
117
|
+
|
|
118
|
+
tokens.map(&:downcase).uniq
|
|
119
|
+
end
|
|
120
|
+
|
|
99
121
|
def hash_parser(property_array)
|
|
100
122
|
h = {}
|
|
101
123
|
unless property_array.nil? || property_array.empty?
|
data/lib/evva/version.rb
CHANGED
data/lib/evva.rb
CHANGED
|
@@ -26,6 +26,7 @@ module Evva
|
|
|
26
26
|
config = Evva::Config.new(hash: YAML.safe_load(config_file))
|
|
27
27
|
bundle = analytics_data(config: config.data_source)
|
|
28
28
|
filter_destinations!(bundle, config.exclude_destinations)
|
|
29
|
+
filter_platforms!(bundle, config.filter_by_platforms)
|
|
29
30
|
case config.type.downcase
|
|
30
31
|
when "android"
|
|
31
32
|
generator = Evva::KotlinGenerator.new(config.package_name)
|
|
@@ -102,6 +103,40 @@ module Evva
|
|
|
102
103
|
bundle[:people].each { |p| p.destinations.reject! { |d| excluded.include?(d) } }
|
|
103
104
|
end
|
|
104
105
|
|
|
106
|
+
def filter_platforms!(bundle, platforms)
|
|
107
|
+
return if platforms.nil? || platforms.empty?
|
|
108
|
+
|
|
109
|
+
normalized = platforms.map { |p| p.to_s.downcase }
|
|
110
|
+
|
|
111
|
+
kept, filtered = bundle[:events].partition { |event|
|
|
112
|
+
normalized.any? { |p| event.supports_platform?(p) }
|
|
113
|
+
}
|
|
114
|
+
return if filtered.empty?
|
|
115
|
+
|
|
116
|
+
bundle[:events] = kept
|
|
117
|
+
Logger.info("Filtered #{filtered.size} events (#{filtered.map(&:event_name).join(', ')})")
|
|
118
|
+
|
|
119
|
+
# Only what the dropped events were keeping alive. An enum that had no
|
|
120
|
+
# reference before this run keeps its place on purpose: pruning those would
|
|
121
|
+
# change the output of sheets that have no Platform column at all.
|
|
122
|
+
orphaned = types_referenced_by(filtered) - types_referenced_by(kept, bundle[:people])
|
|
123
|
+
pruned, remaining = bundle[:enums].partition { |enum| orphaned.include?(enum.enum_name) }
|
|
124
|
+
return if pruned.empty?
|
|
125
|
+
|
|
126
|
+
bundle[:enums] = remaining
|
|
127
|
+
Logger.info("Pruned #{pruned.size} enums (#{pruned.map(&:enum_name).join(', ')})")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Every property type these events and people properties name, with the
|
|
131
|
+
# optional marker stripped. The caller compares against real enum names, so
|
|
132
|
+
# this never has to decide whether a type is an enum. People properties are
|
|
133
|
+
# never platform filtered but still hold references, so an enum kept alive only
|
|
134
|
+
# by one must survive.
|
|
135
|
+
def types_referenced_by(events, people = [])
|
|
136
|
+
types = events.flat_map { |event| event.properties.values } + people.map(&:type)
|
|
137
|
+
types.map { |type| type.to_s.chomp("?") }
|
|
138
|
+
end
|
|
139
|
+
|
|
105
140
|
def write_to_file(path, data)
|
|
106
141
|
file_reader = Evva::FileReader.new
|
|
107
142
|
if file = file_reader.open_file(path, "w", false)
|
data/spec/evva_spec.rb
CHANGED
|
@@ -57,6 +57,189 @@ describe Evva do
|
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
+
describe ".filter_platforms!" do
|
|
61
|
+
def event(name, platforms, properties = {})
|
|
62
|
+
Evva::AnalyticsEvent.new(name, properties, ["firebase"], platforms)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
let(:unrestricted) { event("unrestricted", nil) }
|
|
66
|
+
let(:ios_only) { event("ios_only", ["ios"], { from_screen: "IosOnlyEnum" }) }
|
|
67
|
+
let(:android_only) { event("android_only", ["android"], { from_screen: "AndroidOnlyEnum" }) }
|
|
68
|
+
|
|
69
|
+
let(:events) { [unrestricted, ios_only, android_only] }
|
|
70
|
+
|
|
71
|
+
let(:enums) do
|
|
72
|
+
[
|
|
73
|
+
Evva::AnalyticsEnum.new("IosOnlyEnum", ["a"]),
|
|
74
|
+
Evva::AnalyticsEnum.new("AndroidOnlyEnum", ["b"]),
|
|
75
|
+
Evva::AnalyticsEnum.new("NeverReferenced", ["c"]),
|
|
76
|
+
]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
let(:people) { [Evva::AnalyticsProperty.new("total_friends", "Int", [])] }
|
|
80
|
+
|
|
81
|
+
let(:bundle) do
|
|
82
|
+
{ destinations: ["firebase"], events: events, people: people, enums: enums }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def enum_names
|
|
86
|
+
bundle[:enums].map(&:enum_name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def event_names
|
|
90
|
+
bundle[:events].map(&:event_name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
context "when filtering for iOS" do
|
|
94
|
+
before { Evva.filter_platforms!(bundle, ["ios"]) }
|
|
95
|
+
|
|
96
|
+
it "keeps unrestricted events" do
|
|
97
|
+
expect(event_names).to include("unrestricted")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it "keeps iOS events" do
|
|
101
|
+
expect(event_names).to include("ios_only")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it "drops Android events" do
|
|
105
|
+
expect(event_names).not_to include("android_only")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
context "when filtering for Android" do
|
|
110
|
+
before { Evva.filter_platforms!(bundle, ["android"]) }
|
|
111
|
+
|
|
112
|
+
it "keeps unrestricted events" do
|
|
113
|
+
expect(event_names).to include("unrestricted")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "keeps Android events" do
|
|
117
|
+
expect(event_names).to include("android_only")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "drops iOS events" do
|
|
121
|
+
expect(event_names).not_to include("ios_only")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
context "when filtering for multiple platforms" do
|
|
126
|
+
before { Evva.filter_platforms!(bundle, ["ios", "android"]) }
|
|
127
|
+
|
|
128
|
+
it "keeps events matching any of them" do
|
|
129
|
+
expect(event_names).to eq(["unrestricted", "ios_only", "android_only"])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
context "when filter_by_platforms is nil" do
|
|
134
|
+
before { Evva.filter_platforms!(bundle, nil) }
|
|
135
|
+
|
|
136
|
+
it "does not filter anything" do
|
|
137
|
+
expect(event_names).to eq(["unrestricted", "ios_only", "android_only"])
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
context "when filter_by_platforms is empty" do
|
|
142
|
+
before { Evva.filter_platforms!(bundle, []) }
|
|
143
|
+
|
|
144
|
+
it "does not filter anything" do
|
|
145
|
+
expect(event_names).to eq(["unrestricted", "ios_only", "android_only"])
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "accounts for every event it was given" do
|
|
150
|
+
events_in = bundle[:events].size
|
|
151
|
+
Evva.filter_platforms!(bundle, ["ios"])
|
|
152
|
+
events_out = bundle[:events].size
|
|
153
|
+
events_filtered = events.count { |e| !e.supports_platform?("ios") }
|
|
154
|
+
|
|
155
|
+
expect(events_in).to eq(events_out + events_filtered)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
context "when no event is filtered out" do
|
|
159
|
+
let(:events) { [unrestricted] }
|
|
160
|
+
|
|
161
|
+
it "leaves the events untouched" do
|
|
162
|
+
expect { Evva.filter_platforms!(bundle, ["ios"]) }.not_to change { bundle[:events] }
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it "leaves the enums untouched, orphans included" do
|
|
166
|
+
expect { Evva.filter_platforms!(bundle, ["ios"]) }.not_to change { enum_names }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "logs nothing" do
|
|
170
|
+
expect {
|
|
171
|
+
Evva.filter_platforms!(bundle, ["ios"])
|
|
172
|
+
}.to not_change { Evva::Logger.summary[:info] }
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
describe "enum pruning" do
|
|
177
|
+
it "prunes an enum whose only event was filtered out" do
|
|
178
|
+
Evva.filter_platforms!(bundle, ["ios"])
|
|
179
|
+
|
|
180
|
+
expect(enum_names).not_to include("AndroidOnlyEnum")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "keeps an enum still referenced by a surviving event" do
|
|
184
|
+
Evva.filter_platforms!(bundle, ["ios"])
|
|
185
|
+
|
|
186
|
+
expect(enum_names).to include("IosOnlyEnum")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
it "keeps an enum that was already unreferenced before filtering" do
|
|
190
|
+
Evva.filter_platforms!(bundle, ["ios"])
|
|
191
|
+
|
|
192
|
+
expect(enum_names).to include("NeverReferenced")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
context "when a filtered event's enum is also referenced by a people property" do
|
|
196
|
+
let(:people) do
|
|
197
|
+
[Evva::AnalyticsProperty.new("wearable_platform", "AndroidOnlyEnum", [])]
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "keeps the enum" do
|
|
201
|
+
Evva.filter_platforms!(bundle, ["ios"])
|
|
202
|
+
|
|
203
|
+
expect(enum_names).to include("AndroidOnlyEnum")
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
context "when the reference is optional" do
|
|
208
|
+
let(:android_only) { event("android_only", ["android"], { from_screen: "AndroidOnlyEnum?" }) }
|
|
209
|
+
let(:ios_only) { event("ios_only", ["ios"], { from_screen: "IosOnlyEnum?" }) }
|
|
210
|
+
|
|
211
|
+
it "prunes the filtered platform's enum" do
|
|
212
|
+
Evva.filter_platforms!(bundle, ["ios"])
|
|
213
|
+
|
|
214
|
+
expect(enum_names).not_to include("AndroidOnlyEnum")
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
it "keeps the surviving platform's enum" do
|
|
218
|
+
Evva.filter_platforms!(bundle, ["ios"])
|
|
219
|
+
|
|
220
|
+
expect(enum_names).to include("IosOnlyEnum")
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
describe "logging" do
|
|
226
|
+
subject(:messages) do
|
|
227
|
+
logged = []
|
|
228
|
+
allow(Evva::Logger).to receive(:info) { |msg| logged << msg }
|
|
229
|
+
Evva.filter_platforms!(bundle, ["ios"])
|
|
230
|
+
logged
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "names the filtered events" do
|
|
234
|
+
expect(messages).to include("Filtered 1 events (android_only)")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "names the pruned enums" do
|
|
238
|
+
expect(messages).to include("Pruned 1 enums (AndroidOnlyEnum)")
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
60
243
|
context "when generic.yml does not exist locally" do
|
|
61
244
|
let(:error) { "Could not open yml file" }
|
|
62
245
|
before do
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Event Name,Platform,Event Properties,Added in,Event Destination,Description
|
|
2
|
+
cp_page_view,,"course_id:Long,course_name:String",1.0,"firebase,custom destination",Empty cell means every platform
|
|
3
|
+
ios_only_event,iOS,from_screen:IosOnlyEnum,1.1,firebase,Only tracked on iOS
|
|
4
|
+
android_only_event,Android,from_screen:AndroidOnlyEnum,1.1,firebase,Only tracked on Android
|
|
5
|
+
multi_platform_event,"iOS, Android",source:SharedEnum,1.2,firebase,Both platforms spelled out as a list
|
|
6
|
+
mixed_case_event,IOS,,1.2,firebase,Casing is normalised
|
|
7
|
+
wear_sync_event,iOS,platform:WearableAppPlatform,1.3,firebase,Enum also referenced by a people property
|
|
@@ -65,6 +65,24 @@ describe Evva::Config do
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
describe "#filter_by_platforms" do
|
|
69
|
+
context "when not set" do
|
|
70
|
+
its(:filter_by_platforms) { should be_nil }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context "when set to an array" do
|
|
74
|
+
before { hash[:filter_by_platforms] = ["ios"] }
|
|
75
|
+
|
|
76
|
+
its(:filter_by_platforms) { should eq(["ios"]) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
context "when not an array" do
|
|
80
|
+
before { hash[:filter_by_platforms] = "ios" }
|
|
81
|
+
|
|
82
|
+
it { expect { config }.to raise_error(ArgumentError, /Expected Array, got String/) }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
68
86
|
describe "#swift_public?" do
|
|
69
87
|
context "when swift_public is true" do
|
|
70
88
|
before { hash[:swift_public] = true }
|
|
@@ -23,14 +23,120 @@ describe Evva::GoogleSheet do
|
|
|
23
23
|
|
|
24
24
|
it "returns an array with the corresponding events" do
|
|
25
25
|
expected = [
|
|
26
|
-
Evva::AnalyticsEvent.new("cp_page_view", { course_id: "Long", course_name: "String" }, ["firebase", "custom destination"]),
|
|
27
|
-
Evva::AnalyticsEvent.new("nav_feed_tap", {}, []),
|
|
28
|
-
Evva::AnalyticsEvent.new("cp_view_scorecard", { course_id: "Long", course_name: "String" }, ["custom destination"]),
|
|
29
|
-
Evva::AnalyticsEvent.new("side_game_delete", { fromScreen: "SideGameFromScreen", round_group_creation_token: "String" }, ["firebase"]),
|
|
26
|
+
Evva::AnalyticsEvent.new("cp_page_view", { course_id: "Long", course_name: "String" }, ["firebase", "custom destination"], nil),
|
|
27
|
+
Evva::AnalyticsEvent.new("nav_feed_tap", {}, [], nil),
|
|
28
|
+
Evva::AnalyticsEvent.new("cp_view_scorecard", { course_id: "Long", course_name: "String" }, ["custom destination"], nil),
|
|
29
|
+
Evva::AnalyticsEvent.new("side_game_delete", { fromScreen: "SideGameFromScreen", round_group_creation_token: "String" }, ["firebase"], nil),
|
|
30
30
|
]
|
|
31
31
|
expect(events).to eq(expected)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
context "when the sheet has no Platform column" do
|
|
35
|
+
it "leaves platforms as nil (unrestricted)" do
|
|
36
|
+
expect(events.map(&:platforms)).to all(be_nil)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context "when the sheet has a Platform column" do
|
|
41
|
+
let(:events_file) { File.read("spec/fixtures/sample_public_events_with_platform.csv") }
|
|
42
|
+
|
|
43
|
+
def platforms_for(event_name)
|
|
44
|
+
events.find { |event| event.event_name == event_name }.platforms
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it do
|
|
48
|
+
expect { events }.not_to raise_error
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "reads the column by header name, whatever its position" do
|
|
52
|
+
expect(events.map(&:event_name)).to eq(%w[
|
|
53
|
+
cp_page_view
|
|
54
|
+
ios_only_event
|
|
55
|
+
android_only_event
|
|
56
|
+
multi_platform_event
|
|
57
|
+
mixed_case_event
|
|
58
|
+
wear_sync_event
|
|
59
|
+
])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "treats an empty cell as unrestricted" do
|
|
63
|
+
expect(platforms_for("cp_page_view")).to be_nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "reads a single platform" do
|
|
67
|
+
expect(platforms_for("ios_only_event")).to eq(["ios"])
|
|
68
|
+
expect(platforms_for("android_only_event")).to eq(["android"])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "reads a comma separated list of platforms" do
|
|
72
|
+
expect(platforms_for("multi_platform_event")).to eq(["ios", "android"])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "normalises casing" do
|
|
76
|
+
expect(platforms_for("mixed_case_event")).to eq(["ios"])
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
["platform", "Platform ", " Platform", "PLATFORM", "pLaTfOrM"].each do |header|
|
|
81
|
+
context "when the Platform header is written as #{header.inspect}" do
|
|
82
|
+
let(:events_file) { %(Event Name,"#{header}"\nsome_event,Android\n) }
|
|
83
|
+
|
|
84
|
+
it "still finds the column" do
|
|
85
|
+
expect(events.first.platforms).to eq(["android"])
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
context "when there is no Platform column" do
|
|
91
|
+
let(:events_file) { "Event Name,Event Destination\nsome_event,firebase\n" }
|
|
92
|
+
|
|
93
|
+
it "says so, so that a mistyped header is not mistaken for a filter that ran" do
|
|
94
|
+
logged = []
|
|
95
|
+
allow(Evva::Logger).to receive(:info) { |msg| logged << msg }
|
|
96
|
+
|
|
97
|
+
events
|
|
98
|
+
|
|
99
|
+
expect(logged).to include(/No Platform column/)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
context "when the Platform column is present" do
|
|
104
|
+
let(:events_file) { "Event Name,Platform\nsome_event,iOS\n" }
|
|
105
|
+
|
|
106
|
+
it "does not report a missing column" do
|
|
107
|
+
logged = []
|
|
108
|
+
allow(Evva::Logger).to receive(:info) { |msg| logged << msg }
|
|
109
|
+
|
|
110
|
+
events
|
|
111
|
+
|
|
112
|
+
expect(logged).not_to include(/No Platform column/)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
context "when a platform cell is written in another casing" do
|
|
117
|
+
let(:events_file) { "Event Name,Platform\nsome_event, AnDrOiD \n" }
|
|
118
|
+
|
|
119
|
+
it "normalises it" do
|
|
120
|
+
expect(events.first.platforms).to eq(["android"])
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
context "when a platform cell repeats a platform" do
|
|
125
|
+
let(:events_file) { "Event Name,Platform\nsome_event,\"ios,iOS\"\n" }
|
|
126
|
+
|
|
127
|
+
it "does not duplicate it" do
|
|
128
|
+
expect(events.first.platforms).to eq(["ios"])
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
context "when a platform cell has an unknown token" do
|
|
133
|
+
let(:events_file) { "Event Name,Platform\nsome_event,Windows\n" }
|
|
134
|
+
|
|
135
|
+
it "stores it as-is (lowercased)" do
|
|
136
|
+
expect(events.first.platforms).to eq(["windows"])
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
34
140
|
context "when given an inexistent sheet" do
|
|
35
141
|
before { stub_request(:get, events_sheet).to_return(status: 400, body: "Not Found", headers: {}) }
|
|
36
142
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
describe "filtering by platform end to end" do
|
|
2
|
+
let(:events_url) { "https://wtvr1" }
|
|
3
|
+
let(:people_url) { "https://wtvr2" }
|
|
4
|
+
let(:enum_url) { "https://wtvr3" }
|
|
5
|
+
|
|
6
|
+
let(:events_fixture) { "sample_public_events_with_platform.csv" }
|
|
7
|
+
let(:enums_fixture) { "sample_public_enums_with_platform.csv" }
|
|
8
|
+
|
|
9
|
+
before do
|
|
10
|
+
stub_request(:get, events_url).to_return(status: 200, body: fixture(events_fixture))
|
|
11
|
+
stub_request(:get, people_url).to_return(status: 200, body: fixture("sample_public_people_properties.csv"))
|
|
12
|
+
stub_request(:get, enum_url).to_return(status: 200, body: fixture(enums_fixture))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fixture(name)
|
|
16
|
+
File.read("spec/fixtures/#{name}")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unfiltered_bundle
|
|
20
|
+
source = {
|
|
21
|
+
type: "google_sheet",
|
|
22
|
+
events_url: events_url,
|
|
23
|
+
people_properties_url: people_url,
|
|
24
|
+
enum_classes_url: enum_url
|
|
25
|
+
}
|
|
26
|
+
Evva.analytics_data(config: source)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def bundle_for(platforms)
|
|
30
|
+
bundle = unfiltered_bundle
|
|
31
|
+
Evva.filter_platforms!(bundle, platforms)
|
|
32
|
+
bundle
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def logged_while
|
|
36
|
+
logged = []
|
|
37
|
+
allow(Evva::Logger).to receive(:info) { |msg| logged << msg }
|
|
38
|
+
yield
|
|
39
|
+
logged
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe "for iOS" do
|
|
43
|
+
let(:bundle) { bundle_for(["ios"]) }
|
|
44
|
+
|
|
45
|
+
it "keeps the iOS and unrestricted events" do
|
|
46
|
+
expect(bundle[:events].map(&:event_name)).to eq(%w[
|
|
47
|
+
cp_page_view
|
|
48
|
+
ios_only_event
|
|
49
|
+
multi_platform_event
|
|
50
|
+
mixed_case_event
|
|
51
|
+
wear_sync_event
|
|
52
|
+
])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "accounts for every event it was given" do
|
|
56
|
+
filtered = logged_while { bundle }.grep(/^Filtered/).first.to_s[/\((.*)\)/, 1].to_s.split(", ")
|
|
57
|
+
|
|
58
|
+
expect(bundle[:events].size + filtered.size).to eq(unfiltered_bundle[:events].size)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "prunes the Android only enum and keeps the rest" do
|
|
62
|
+
expect(bundle[:enums].map(&:enum_name)).to eq(%w[
|
|
63
|
+
PageViewSourceScreen
|
|
64
|
+
IosOnlyEnum
|
|
65
|
+
SharedEnum
|
|
66
|
+
WearableAppPlatform
|
|
67
|
+
])
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
describe "for Android" do
|
|
72
|
+
let(:bundle) { bundle_for(["android"]) }
|
|
73
|
+
|
|
74
|
+
it "keeps the Android and unrestricted events" do
|
|
75
|
+
expect(bundle[:events].map(&:event_name)).to eq(%w[
|
|
76
|
+
cp_page_view
|
|
77
|
+
android_only_event
|
|
78
|
+
multi_platform_event
|
|
79
|
+
])
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "prunes the iOS only enum" do
|
|
83
|
+
expect(bundle[:enums].map(&:enum_name)).not_to include("IosOnlyEnum")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# wear_sync_event is iOS only and gets filtered here, but the
|
|
87
|
+
# wearable_platform people property still references the enum.
|
|
88
|
+
it "keeps an enum held only by a people property" do
|
|
89
|
+
expect(bundle[:enums].map(&:enum_name)).to include("WearableAppPlatform")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "keeps the enum no event ever referenced" do
|
|
93
|
+
expect(bundle[:enums].map(&:enum_name)).to include("PageViewSourceScreen")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe "for multiple platforms" do
|
|
98
|
+
let(:bundle) { bundle_for(["ios", "android"]) }
|
|
99
|
+
|
|
100
|
+
it "keeps events matching any of the platforms" do
|
|
101
|
+
expect(bundle[:events].map(&:event_name)).to eq(%w[
|
|
102
|
+
cp_page_view
|
|
103
|
+
ios_only_event
|
|
104
|
+
android_only_event
|
|
105
|
+
multi_platform_event
|
|
106
|
+
mixed_case_event
|
|
107
|
+
wear_sync_event
|
|
108
|
+
])
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# What protects the sheets that predate the column.
|
|
113
|
+
# The generators are pure functions of the bundle and are untouched here, so a
|
|
114
|
+
# bundle the filter left alone generates exactly what it generated before.
|
|
115
|
+
describe "a sheet with no Platform column" do
|
|
116
|
+
let(:events_fixture) { "sample_public_events.csv" }
|
|
117
|
+
let(:enums_fixture) { "sample_public_enums.csv" }
|
|
118
|
+
|
|
119
|
+
[["ios"], ["android"]].each do |platforms|
|
|
120
|
+
it "leaves the #{platforms} bundle untouched" do
|
|
121
|
+
before_filtering = unfiltered_bundle
|
|
122
|
+
after_filtering = bundle_for(platforms)
|
|
123
|
+
|
|
124
|
+
expect(after_filtering[:events].map(&:event_name)).to eq(before_filtering[:events].map(&:event_name))
|
|
125
|
+
expect(after_filtering[:enums].map(&:enum_name)).to eq(before_filtering[:enums].map(&:enum_name))
|
|
126
|
+
expect(after_filtering[:people].map(&:property_name)).to eq(before_filtering[:people].map(&:property_name))
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
it "keeps enums that no event references" do
|
|
131
|
+
expect(bundle_for(["ios"])[:enums].map(&:enum_name)).to eq(%w[PageViewSourceScreen PremiumClickBuy])
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "reports no filtering" do
|
|
135
|
+
expect(logged_while { bundle_for(["ios"]) }.grep(/filtered|pruned/)).to be_empty
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe "when filter_by_platforms is nil" do
|
|
140
|
+
it "leaves the bundle untouched" do
|
|
141
|
+
before_filtering = unfiltered_bundle
|
|
142
|
+
after_filtering = bundle_for(nil)
|
|
143
|
+
|
|
144
|
+
expect(after_filtering[:events].map(&:event_name)).to eq(before_filtering[:events].map(&:event_name))
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: evva
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- André Andrade
|
|
@@ -10,7 +10,7 @@ authors:
|
|
|
10
10
|
autorequire:
|
|
11
11
|
bindir: bin
|
|
12
12
|
cert_chain: []
|
|
13
|
-
date: 2026-
|
|
13
|
+
date: 2026-09-04 00:00:00.000000000 Z
|
|
14
14
|
dependencies:
|
|
15
15
|
- !ruby/object:Gem::Dependency
|
|
16
16
|
name: colorize
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
name: csv
|
|
31
31
|
requirement: !ruby/object:Gem::Requirement
|
|
32
32
|
requirements:
|
|
33
|
-
- - "
|
|
33
|
+
- - "~>"
|
|
34
34
|
- !ruby/object:Gem::Version
|
|
35
35
|
version: '3.0'
|
|
36
36
|
type: :runtime
|
|
37
37
|
prerelease: false
|
|
38
38
|
version_requirements: !ruby/object:Gem::Requirement
|
|
39
39
|
requirements:
|
|
40
|
-
- - "
|
|
40
|
+
- - "~>"
|
|
41
41
|
- !ruby/object:Gem::Version
|
|
42
42
|
version: '3.0'
|
|
43
43
|
- !ruby/object:Gem::Dependency
|
|
@@ -99,7 +99,9 @@ files:
|
|
|
99
99
|
- lib/evva/version.rb
|
|
100
100
|
- spec/evva_spec.rb
|
|
101
101
|
- spec/fixtures/sample_public_enums.csv
|
|
102
|
+
- spec/fixtures/sample_public_enums_with_platform.csv
|
|
102
103
|
- spec/fixtures/sample_public_events.csv
|
|
104
|
+
- spec/fixtures/sample_public_events_with_platform.csv
|
|
103
105
|
- spec/fixtures/sample_public_people_properties.csv
|
|
104
106
|
- spec/fixtures/test.yml
|
|
105
107
|
- spec/lib/evva/config_spec.rb
|
|
@@ -108,6 +110,7 @@ files:
|
|
|
108
110
|
- spec/lib/evva/logger_spec.rb
|
|
109
111
|
- spec/lib/evva/object_extension_spec.rb
|
|
110
112
|
- spec/lib/evva/swift_generator_spec.rb
|
|
113
|
+
- spec/platform_filter_spec.rb
|
|
111
114
|
- spec/spec_helper.rb
|
|
112
115
|
homepage: https://github.com/hole19/
|
|
113
116
|
licenses:
|