Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Layout/SpaceInsideHashLiteralBraces:
Exclude:
- 'lib/evva/config.rb'
- 'lib/evva/logger.rb'
- 'spec/evva_spec.rb'
- 'spec/lib/evva/config_spec.rb'
- 'spec/lib/evva/google_sheet_spec.rb'
- 'spec/lib/evva/kotlin_generator_spec.rb'
Expand Down Expand Up @@ -253,6 +254,8 @@ Style/HashSyntax:
- 'Gemfile'
- 'Rakefile'
- 'lib/evva.rb'
- 'spec/evva_spec.rb'
- 'spec/platform_filter_spec.rb'
- 'lib/evva/config.rb'
- 'lib/evva/google_sheet.rb'
- 'lib/evva/kotlin_generator.rb'
Expand Down Expand Up @@ -359,6 +362,7 @@ Style/SymbolProc:
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArrayLiteral:
Exclude:
- 'spec/evva_spec.rb'
- 'spec/lib/evva/google_sheet_spec.rb'
- 'spec/lib/evva/kotlin_generator_spec.rb'
- 'spec/lib/evva/swift_generator_spec.rb'
Expand All @@ -370,6 +374,7 @@ Style/TrailingCommaInArrayLiteral:
Style/TrailingCommaInHashLiteral:
Exclude:
- 'lib/evva/config.rb'
- 'spec/evva_spec.rb'
- 'lib/evva/kotlin_generator.rb'
- 'lib/evva/swift_generator.rb'
- 'spec/lib/evva/config_spec.rb'
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
evva (0.8.4)
evva (0.8.5)
colorize (~> 1.0)
csv (>= 3.0)
safe_yaml (~> 1.0)
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,43 @@ Evva automatically generates code for triggering events based on a Google Sheets
destinations_file_name: /file/with/destinations
special_enum_file_name: /file/with/special/enum/properties/
swift_public: false # optional; when true (iOS), generated Swift uses the public access modifier for generated extensions
filter_by_platforms: # optional; array of platforms to keep (e.g. [ios])
- ios
```

## The events sheet

The events sheet is read by column header, so column order does not matter and
unknown columns are ignored.

| Column | Required | Meaning |
| --- | --- | --- |
| `Event Name` | yes | The event name |
| `Event Properties` | no | Comma separated `name:Type` pairs |
| `Event Destination` | no | Comma separated destinations |
| `Platform` | no | Which platforms track the event |

### Platform

`Platform` tags an event with the platforms it belongs to. Filtering is driven
by the `filter_by_platforms` key in `evva_config.yml`: only events matching one
of the listed platforms (or unrestricted events) are generated. When the key is
absent, every event is generated.

Any value is accepted as a platform name (case insensitive, comma separated).
An empty cell means the event is unrestricted and will never be filtered out.

The header is matched ignoring case and surrounding whitespace, so `platform` and
`Platform ` are both found.

When no `Platform` column is found the run says so, because "every event was
generated" is otherwise indistinguishable from a header that failed to match:

```
[INFO] No Platform column in the events sheet, every event will be generated for every platform
```

Enums whose only referencing events were filtered out are pruned, unless a
people property also references them.

People properties have no `Platform` column and are never filtered.
35 changes: 35 additions & 0 deletions lib/evva.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def run(options)
config = Evva::Config.new(hash: YAML.safe_load(config_file))
bundle = analytics_data(config: config.data_source)
filter_destinations!(bundle, config.exclude_destinations)
filter_platforms!(bundle, config.filter_by_platforms)
case config.type.downcase
when "android"
generator = Evva::KotlinGenerator.new(config.package_name)
Expand Down Expand Up @@ -102,6 +103,40 @@ def filter_destinations!(bundle, excluded)
bundle[:people].each { |p| p.destinations.reject! { |d| excluded.include?(d) } }
end

def filter_platforms!(bundle, platforms)
return if platforms.nil? || platforms.empty?

normalized = platforms.map { |p| p.to_s.downcase }

kept, filtered = bundle[:events].partition { |event|
normalized.any? { |p| event.supports_platform?(p) }
}
return if filtered.empty?

bundle[:events] = kept
Logger.info("Filtered #{filtered.size} events (#{filtered.map(&:event_name).join(', ')})")

# Only what the dropped events were keeping alive. An enum that had no
# reference before this run keeps its place on purpose: pruning those would
# change the output of sheets that have no Platform column at all.
orphaned = types_referenced_by(filtered) - types_referenced_by(kept, bundle[:people])
pruned, remaining = bundle[:enums].partition { |enum| orphaned.include?(enum.enum_name) }
return if pruned.empty?

bundle[:enums] = remaining
Logger.info("Pruned #{pruned.size} enums (#{pruned.map(&:enum_name).join(', ')})")
end

# Every property type these events and people properties name, with the
# optional marker stripped. The caller compares against real enum names, so
# this never has to decide whether a type is an enum. People properties are
# never platform filtered but still hold references, so an enum kept alive only
# by one must survive.
def types_referenced_by(events, people = [])
types = events.flat_map { |event| event.properties.values } + people.map(&:type)
types.map { |type| type.to_s.chomp("?") }
end

def write_to_file(path, data)
file_reader = Evva::FileReader.new
if file = file_reader.open_file(path, "w", false)
Expand Down
14 changes: 11 additions & 3 deletions lib/evva/analytics_event.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
module Evva
class AnalyticsEvent
attr_reader :event_name, :properties, :destinations
attr_reader :event_name, :properties, :destinations, :platforms

def initialize(event_name, properties, destinations)
def initialize(event_name, properties, destinations, platforms = nil)
@event_name = event_name
@properties = properties
@destinations = destinations
@platforms = platforms
end

def supports_platform?(platform)
return true if platforms.nil?

platforms.include?(platform.to_s.downcase)
end

def ==(other)
event_name == other.event_name &&
properties == other.properties &&
destinations == other.destinations
destinations == other.destinations &&
platforms == other.platforms
end
end
end
7 changes: 6 additions & 1 deletion lib/evva/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ def exclude_destinations
@hash[:exclude_destinations] || []
end

def filter_by_platforms
@hash[:filter_by_platforms]
end

CONFIG_STRUCT = {
type: Hash,
elements: {
Expand All @@ -83,7 +87,8 @@ def exclude_destinations
destinations_file_name: { type: String },
package_name: { type: String },
swift_public: { type: Object, optional: true },
exclude_destinations: { type: Array, optional: true }
exclude_destinations: { type: Array, optional: true },
filter_by_platforms: { type: Array, optional: true }
}
}.freeze

Expand Down
32 changes: 27 additions & 5 deletions lib/evva/google_sheet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class GoogleSheet
EVENT_NAME = "Event Name"
EVENT_PROPERTIES = "Event Properties"
EVENT_DESTINATION = "Event Destination"
EVENT_PLATFORM = "Platform"

PROPERTY_NAME = "Property Name"
PROPERTY_TYPE = "Property Type"
Expand All @@ -26,11 +27,17 @@ def events
get_csv(@events_url)
end

@events ||= @events_csv.map do |row|
event_name = row[EVENT_NAME]
properties = hash_parser(row[EVENT_PROPERTIES])
destinations = row[EVENT_DESTINATION]&.split(",")
Evva::AnalyticsEvent.new(event_name, properties, destinations || [])
@events ||= begin
platform_header = header_matching(@events_csv, EVENT_PLATFORM)
Logger.info("No #{EVENT_PLATFORM} column in the events sheet, every event will be generated for every platform") if platform_header.nil?

@events_csv.map do |row|
event_name = row[EVENT_NAME]
properties = hash_parser(row[EVENT_PROPERTIES])
destinations = row[EVENT_DESTINATION]&.split(",")
platforms = platform_parser(platform_header && row[platform_header])
Evva::AnalyticsEvent.new(event_name, properties, destinations || [], platforms)
end
end
end

Expand Down Expand Up @@ -96,6 +103,21 @@ def get(url, max_redirects = 1)
response.body
end

# Matched ignoring case and surrounding whitespace, because a header of
# "platform" or "Platform " means what it plainly means.
def header_matching(csv, name)
csv.headers.compact.find { |header| header.to_s.strip.casecmp?(name) }
end

# nil (no Platform column or empty cell) means unrestricted (never filtered out).
# Concrete platform tokens are stored lowercased and deduplicated.
def platform_parser(platform_list)
tokens = platform_list.to_s.split(",").map(&:strip).reject(&:empty?)
return nil if tokens.empty?

tokens.map(&:downcase).uniq
end

def hash_parser(property_array)
h = {}
unless property_array.nil? || property_array.empty?
Expand Down
Loading
Loading