gaq 0.2.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.
Files changed (70) hide show
  1. data/.gitignore +18 -0
  2. data/Gemfile +15 -0
  3. data/Guardfile +23 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +102 -0
  6. data/Rakefile +1 -0
  7. data/gaq.gemspec +22 -0
  8. data/lib/gaq.rb +3 -0
  9. data/lib/gaq/class_cache.rb +32 -0
  10. data/lib/gaq/command_language.rb +145 -0
  11. data/lib/gaq/configuration.rb +159 -0
  12. data/lib/gaq/controller_facade.rb +15 -0
  13. data/lib/gaq/controller_handle.rb +93 -0
  14. data/lib/gaq/dsl_target.rb +51 -0
  15. data/lib/gaq/dsl_target_factory.rb +64 -0
  16. data/lib/gaq/flash_commands_adapter.rb +48 -0
  17. data/lib/gaq/interprets_config.rb +13 -0
  18. data/lib/gaq/railtie.rb +40 -0
  19. data/lib/gaq/snippet_renderer.rb +44 -0
  20. data/lib/gaq/version.rb +3 -0
  21. data/spec-dummy/.rspec +1 -0
  22. data/spec-dummy/README.rdoc +261 -0
  23. data/spec-dummy/Rakefile +7 -0
  24. data/spec-dummy/app/assets/images/rails.png +0 -0
  25. data/spec-dummy/app/assets/javascripts/application.js +13 -0
  26. data/spec-dummy/app/assets/stylesheets/application.css +13 -0
  27. data/spec-dummy/app/controllers/application_controller.rb +3 -0
  28. data/spec-dummy/app/controllers/integration_spec_controller.rb +22 -0
  29. data/spec-dummy/app/helpers/application_helper.rb +2 -0
  30. data/spec-dummy/app/views/integration_spec/view.erb +0 -0
  31. data/spec-dummy/app/views/layouts/application.html.erb +16 -0
  32. data/spec-dummy/config.ru +4 -0
  33. data/spec-dummy/config/application.rb +64 -0
  34. data/spec-dummy/config/boot.rb +6 -0
  35. data/spec-dummy/config/environment.rb +5 -0
  36. data/spec-dummy/config/environments/development.rb +26 -0
  37. data/spec-dummy/config/environments/production.rb +51 -0
  38. data/spec-dummy/config/environments/test_dynamic.rb +39 -0
  39. data/spec-dummy/config/environments/test_static.rb +38 -0
  40. data/spec-dummy/config/initializers/backtrace_silencers.rb +7 -0
  41. data/spec-dummy/config/initializers/inflections.rb +15 -0
  42. data/spec-dummy/config/initializers/mime_types.rb +5 -0
  43. data/spec-dummy/config/initializers/secret_token.rb +7 -0
  44. data/spec-dummy/config/initializers/session_store.rb +8 -0
  45. data/spec-dummy/config/initializers/wrap_parameters.rb +10 -0
  46. data/spec-dummy/config/locales/en.yml +5 -0
  47. data/spec-dummy/config/routes.rb +8 -0
  48. data/spec-dummy/db/seeds.rb +7 -0
  49. data/spec-dummy/public/404.html +26 -0
  50. data/spec-dummy/public/422.html +26 -0
  51. data/spec-dummy/public/500.html +25 -0
  52. data/spec-dummy/public/favicon.ico +0 -0
  53. data/spec-dummy/public/robots.txt +5 -0
  54. data/spec-dummy/script/rails +6 -0
  55. data/spec-dummy/spec/common_spec_methods.rb +88 -0
  56. data/spec-dummy/spec/features/dynamic_config_spec.rb +21 -0
  57. data/spec-dummy/spec/features/next_request_spec.rb +27 -0
  58. data/spec-dummy/spec/features/presence_spec.rb +64 -0
  59. data/spec-dummy/spec/spec_helper.rb +41 -0
  60. data/spec/lib/gaq/class_cache_spec.rb +62 -0
  61. data/spec/lib/gaq/command_language_spec.rb +267 -0
  62. data/spec/lib/gaq/configuration_spec.rb +233 -0
  63. data/spec/lib/gaq/controller_facade_spec.rb +29 -0
  64. data/spec/lib/gaq/controller_handle_spec.rb +510 -0
  65. data/spec/lib/gaq/dsl_target_factory_spec.rb +163 -0
  66. data/spec/lib/gaq/dsl_target_spec.rb +87 -0
  67. data/spec/lib/gaq/flash_commands_adapter_spec.rb +116 -0
  68. data/spec/lib/gaq/interprets_config_spec.rb +37 -0
  69. data/spec/lib/gaq/snippet_renderer_spec.rb +60 -0
  70. metadata +159 -0
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ spec-dummy/log
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in gaq.gemspec
4
+ gemspec
5
+
6
+ gem 'rails', '~> 3.1.0'
7
+ gem 'rspec-rails', '~> 2.13.2'
8
+
9
+ gem 'capybara', '~> 2.0.0'
10
+ gem 'terminal-notifier-guard'
11
+ gem 'rb-fsevent' # used by guard for watching
12
+ gem 'guard-rspec'
13
+ gem 'debugger'
14
+
15
+ gem 'nokogiri'
@@ -0,0 +1,23 @@
1
+ binstubs = nil
2
+
3
+ bundle_config_path = Pathname.new(__FILE__) + '../.bundle/config'
4
+ if File.exist?(bundle_config_path)
5
+ yaml = File.read(bundle_config_path)
6
+ binstubs = YAML.load(yaml)["BUNDLE_BIN"]
7
+ end
8
+
9
+ guard 'rspec', binstubs: binstubs do
10
+ watch(%r{^spec/.+_spec\.rb$})
11
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
12
+ watch('spec/spec_helper.rb') { "spec" }
13
+ end
14
+
15
+ guard 'rspec', :spec_paths => "spec-dummy/spec", cli: "-I spec-dummy/spec --tag ~dynamic",
16
+ binstubs: binstubs,
17
+ env: { 'RAILS_ENV' => 'test_static' } do
18
+ end
19
+
20
+ guard 'rspec', :spec_paths => "spec-dummy/spec", cli: "-I spec-dummy/spec --tag ~static",
21
+ binstubs: binstubs,
22
+ env: { 'RAILS_ENV' => 'test_dynamic' } do
23
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Thomas Stratmann
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,102 @@
1
+ # Gaq
2
+
3
+ Ever wanted to push a track event from a controller? Set a custom variable from model data? Now you can.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'gaq'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install gaq
18
+
19
+ ## Setup
20
+
21
+ 1. Require `gaq` from your `application.rb` and configure your web_property_id like this:
22
+
23
+ ```ruby
24
+ class MyApplication < Rails::Application
25
+ config.gaq.web_property_id = 'UA-XXYOURID-1'
26
+ end
27
+ ```
28
+
29
+ 2. Put this in your application layout:
30
+
31
+ ```ruby
32
+ <%= render_gaq %>
33
+ ```
34
+
35
+ This inserts javascript code for initializing _gaq, your tracking events and the ga.js snippet.
36
+ The ga.js snippet will only be rendered in the production environment, so no real tracking happens
37
+ during development.
38
+
39
+ 3. DONE!
40
+
41
+ ### More setup
42
+
43
+ If you want to use custom variables, configure them like this:
44
+
45
+ ```ruby
46
+ config.gaq.declare_variable :user_gender, scope: :session, slot: 1
47
+ ```
48
+
49
+ If you need the _anonymizeIp feature, enable it like this:
50
+
51
+ ```ruby
52
+ config.gaq.anonymize_ip = true
53
+ ```
54
+
55
+ ## Usage in the controller
56
+
57
+ For inserting a track event to be rendered on the current request, do
58
+
59
+ ```ruby
60
+ gaq.track_event 'category', 'action', 'label'
61
+ ```
62
+
63
+ If you have configured a custom variable like above, do this to set it:
64
+
65
+ ```ruby
66
+ gaq.user_gender = 'female'
67
+ ```
68
+
69
+ If you need to do any of these before a redirect, use these methods on `gaq.next_request`
70
+ instead of `gaq`:
71
+
72
+ ```ruby
73
+ gaq.next_request.track_event 'category', 'action', 'label'
74
+ ```
75
+
76
+ This feature uses the flash for storing _gaq items between requests.
77
+
78
+ ## Supported tracker commands
79
+
80
+ Currently, only _trackEvent and _setCustomVar is supported. However commands are easily added, so open a pull request!
81
+
82
+ ## Contributing
83
+
84
+ 1. Fork it
85
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
86
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
87
+ 4. Push to the branch (`git push origin my-new-feature`)
88
+ 5. Create new Pull Request
89
+
90
+ ### Testing
91
+
92
+ Test dependencies are declared in the Gemfile.
93
+ All specs are run from guard.
94
+
95
+ The most interesting part is the controller_handle_spec, it asserts what commands get rendered under which circumstances.
96
+
97
+ There is a dummy rails application
98
+ in `spec-dummy` for integration tests, which we need because gaq keeps state in
99
+ the session. The integration specs are located inside of it.
100
+
101
+ It has two test environments, `test_static` and `test_dynamic`. Specs tagged with
102
+ `:static` will not be run under `test_dynamic` and vice versa. The dynamic tests are for dynamic configuration items, which is an upcoming feature that lets you configure things dynamically in the context of the running controller.
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'gaq/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "gaq"
8
+ gem.version = Gaq::VERSION
9
+ gem.authors = ["Thomas Stratmann"]
10
+ gem.email = ["thomas.stratmann@9elements.com"]
11
+ gem.description = %q{Gaq is a lightweight gem for support of pushing static and dynamic data to the _gaq from the backend.}
12
+ gem.summary = %q{Renders _gaq initialization and the ga.js snippet. Supports pushing from the back end}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'actionpack'
21
+ gem.add_dependency 'activesupport'
22
+ end
@@ -0,0 +1,3 @@
1
+ require 'gaq/version'
2
+
3
+ require 'gaq/railtie'
@@ -0,0 +1,32 @@
1
+ module Gaq
2
+ class ClassCache
3
+ Miss = Class.new(RuntimeError)
4
+
5
+ def initialize
6
+ @cached = Hash.new { |hash, key| hash[key] = build(key) }
7
+ @build_instructions = {}
8
+ end
9
+
10
+ def building(key, base_class, &block)
11
+ @build_instructions[key] = [base_class, block]
12
+ self
13
+ end
14
+
15
+ def [](key)
16
+ @cached[key]
17
+ end
18
+
19
+ def self.singleton
20
+ @singleton ||= new
21
+ end
22
+
23
+ private
24
+
25
+ def build(key)
26
+ raise Miss, "Nothing registered for key #{key.inspect}" unless @build_instructions.key?(key)
27
+ base_class, block = @build_instructions[key]
28
+
29
+ Class.new(base_class).tap(&block)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,145 @@
1
+ module Gaq
2
+ class CommandLanguage
3
+ attr_writer :value_coercer
4
+
5
+ def initialize
6
+ @descriptors = {}
7
+ end
8
+
9
+ CommandDescriptor = Struct.new(:signature, :name, :identifier, :sort_slot)
10
+
11
+ def knows_command(identifier)
12
+ @descriptors[identifier] = CommandDescriptor.new.tap do |desc|
13
+ desc.identifier = identifier
14
+ yield desc
15
+ end
16
+ self
17
+ end
18
+
19
+ def commands_to_flash_items(commands)
20
+ commands.map do |command|
21
+ command_to_segments(command)
22
+ end
23
+ end
24
+
25
+ # this happens to be the same, but may be different in the future
26
+ alias_method :commands_to_segments_for_to_json, :commands_to_flash_items
27
+
28
+ def commands_from_flash_items(flash_items)
29
+ flash_items.map do |flash_item|
30
+ descriptor, tracker_name = descriptor_and_tracker_name_from_first_segment(flash_item.first)
31
+ params = flash_item.drop(1).take(descriptor.signature.length)
32
+ Command.new(descriptor, descriptor.name, params, tracker_name)
33
+ end
34
+ end
35
+
36
+ # modifies commands
37
+ def sort_commands(commands)
38
+ sorted_pairs = commands.each_with_index.sort_by do |command, index|
39
+ [
40
+ command.descriptor.sort_slot || sort_slot_fallback,
41
+ index
42
+ ]
43
+ end
44
+ commands.replace sorted_pairs.map(&:first)
45
+ end
46
+
47
+ def new_command(identifier, *params)
48
+ descriptor = @descriptors.fetch(identifier) { raise "no command with identifier #{identifier.inspect}" }
49
+ params = coerce_params(params, descriptor.signature)
50
+
51
+ Command.new(descriptor, descriptor.name, params)
52
+ end
53
+
54
+ Command = Struct.new(:descriptor, :name, :params, :tracker_name)
55
+
56
+ private
57
+
58
+ def sort_slot_fallback
59
+ @sort_slot_fallback ||= @descriptors.values.map(&:sort_slot).compact.max + 1
60
+ end
61
+
62
+ def command_to_segments(command)
63
+ descriptor = command.descriptor
64
+
65
+ first_segment = first_segment_from_descriptor_and_tracker_name(descriptor, command.tracker_name)
66
+ [first_segment, *command.params.take(descriptor.signature.length)]
67
+ end
68
+
69
+ def first_segment_from_descriptor_and_tracker_name(descriptor, tracker_name)
70
+ [tracker_name, descriptor.name].compact.join('.')
71
+ end
72
+
73
+ def descriptor_and_tracker_name_from_first_segment(first_segment)
74
+ split = first_segment.split('.')
75
+ command_name, tracker_name = split.reverse
76
+ descriptor = @descriptors.values.find { |desc| desc.name == command_name }
77
+ [descriptor, tracker_name]
78
+ end
79
+
80
+ def coerce_params(params, signature)
81
+ signature = signature.take(params.length)
82
+ signature.zip(params).map do |type, param|
83
+ @value_coercer.call(type, param)
84
+ end
85
+ end
86
+
87
+ def self.declare_language_on(instance)
88
+ instance.knows_command(:set_account) do |desc|
89
+ desc.name = "_setAccount"
90
+ desc.signature = [:String]
91
+ desc.sort_slot = 0
92
+ end
93
+
94
+ instance.knows_command(:track_pageview) do |desc|
95
+ desc.name = "_trackPageview"
96
+ desc.signature = [:String]
97
+ desc.sort_slot = 2
98
+ end
99
+
100
+ instance.knows_command(:track_event) do |desc|
101
+ desc.name = "_trackEvent"
102
+ desc.signature = [:String, :String, :String, :Int, :Boolean]
103
+ end
104
+
105
+ instance.knows_command(:set_custom_var) do |desc|
106
+ desc.name = "_setCustomVar"
107
+ desc.signature = [:Int, :String, :String, :Int]
108
+ desc.sort_slot = 3
109
+ end
110
+
111
+ instance.knows_command(:anonymize_ip) do |desc|
112
+ desc.name = "_gat._anonymizeIp"
113
+ desc.signature = []
114
+ desc.sort_slot = 1
115
+ end
116
+ end
117
+
118
+ def self.define_transformations_on(instance)
119
+ instance.value_coercer = method(:coerce_value)
120
+ end
121
+
122
+ def self.coerce_value(type, value)
123
+ case type
124
+ when :Boolean
125
+ !!value
126
+ when :String
127
+ String(value)
128
+ when :Int
129
+ Integer(value)
130
+ when :Number
131
+ raise "'Number' coercion not implemented"
132
+ # GA docs do not tell us what that means
133
+ else
134
+ raise "Unable to coerce unknown type #{type.inspect}"
135
+ end
136
+ end
137
+
138
+ def self.singleton
139
+ new.tap do |result|
140
+ declare_language_on(result)
141
+ define_transformations_on(result)
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,159 @@
1
+ require 'forwardable'
2
+ require 'active_support/ordered_options'
3
+
4
+ module Gaq
5
+ class Configuration
6
+ attr_reader :rails_config, :variables
7
+
8
+ RAILS_CONFIG_ACCESSORS = [:anonymize_ip, :render_ga_js]
9
+ attr_accessor(*RAILS_CONFIG_ACCESSORS)
10
+
11
+ VariableException = Class.new(RuntimeError)
12
+
13
+ Variable = Struct.new(:slot, :name, :scope) do
14
+ SCOPE_MAP = {
15
+ nil => 3, #we allow for a default @TODO check documentation
16
+
17
+ :visitor => 1,
18
+ :session => 2,
19
+ :page => 3,
20
+
21
+ 1 => 1,
22
+ 2 => 2,
23
+ 3 => 3
24
+ }
25
+
26
+ def initialize(slot, name, scope)
27
+ super(slot, name)
28
+ set_scope scope
29
+ end
30
+
31
+ private
32
+
33
+ def set_scope(scope)
34
+ self.scope = SCOPE_MAP.fetch(scope) do
35
+ raise VariableException, "unknown scope #{scope.inspect}"
36
+ end
37
+ end
38
+ end
39
+
40
+ def initialize
41
+ default_tracker_config = TrackerConfig.new(nil)
42
+ @default_tracker_rails_config = default_tracker_config.rails_config
43
+
44
+ @tracker_configs = { nil => default_tracker_config }
45
+ @rails_config = RailsConfig.new(self, default_tracker_config)
46
+ @variables = {}
47
+ end
48
+
49
+ def declare_variable(name, options = {})
50
+ # @TODO deprecate use without slot given
51
+ # We just code like it's always given here
52
+ slot = options[:slot]
53
+ # @TODO raise when slot off limits
54
+ raise VariableException, "Already have a variable at that slot" if
55
+ @variables.find { |_, var| var.slot == slot }
56
+ raise VariableException, "Already have a variable of that name" if
57
+ @variables.find { |_, var| var.name == name }
58
+
59
+ variable = Variable.new(slot, name, options[:scope])
60
+ @variables[name] = variable
61
+ end
62
+
63
+ def register_tracker_name(name)
64
+ name = name.to_s
65
+ # @TODO check for collision, assert name format
66
+
67
+ raise "duplicate tracker name" if @tracker_configs.key?(name)
68
+ @tracker_configs[name] = TrackerConfig.new(name)
69
+ end
70
+
71
+ def tracker_rails_config(name) # name can be nil -> default tracker
72
+ name = name.to_s unless name.nil?
73
+ tracker_config = @tracker_configs.fetch(name) { raise "No tracker by that name (#{name.inspect})" }
74
+ tracker_config.rails_config
75
+ end
76
+
77
+ def tracker_config(name)
78
+ @tracker_configs[name]
79
+ end
80
+
81
+ def tracker_names
82
+ @tracker_configs.keys
83
+ end
84
+
85
+ def render_ga_js?(environment)
86
+ environment = environment.to_s
87
+
88
+ case render_ga_js
89
+ when TrueClass, FalseClass
90
+ render_ga_js
91
+ when Array, Symbol
92
+ Array(render_ga_js).map(&:to_s).include? environment
93
+ else
94
+ render_ga_js
95
+ end
96
+ end
97
+
98
+ class TrackerConfig
99
+ attr_reader :rails_config, :tracker_name
100
+
101
+ RAILS_CONFIG_ACCESSORS = [:web_property_id, :track_pageview]
102
+ attr_accessor(*RAILS_CONFIG_ACCESSORS)
103
+ alias_method :track_pageview?, :track_pageview
104
+
105
+ def initialize(tracker_name)
106
+ @tracker_name = tracker_name
107
+
108
+ @track_pageview = true
109
+ @rails_config = RailsConfig.new(self)
110
+
111
+ @web_property_id = 'UA-XUNSET-S'
112
+ end
113
+
114
+ class RailsConfig
115
+ extend Forwardable
116
+ def_delegators :@config, *RAILS_CONFIG_ACCESSORS.map { |m| "#{m}=" }
117
+
118
+ def initialize(config)
119
+ @config = config
120
+ end
121
+ end
122
+ end
123
+
124
+ class RailsConfig
125
+ extend Forwardable
126
+ def_delegators :@config, :declare_variable,
127
+ *Configuration::RAILS_CONFIG_ACCESSORS.map { |m| "#{m}=" }
128
+ def_delegators :@default_tracker_rails_config,
129
+ *TrackerConfig::RAILS_CONFIG_ACCESSORS.map { |m| "#{m}=" }
130
+
131
+ def initialize(config, default_tracker_rails_config)
132
+ @config = config
133
+
134
+ @default_tracker_rails_config = default_tracker_rails_config
135
+
136
+ @anonymize_ip = false
137
+ @render_ga_js = :production
138
+ end
139
+
140
+
141
+ def additional_trackers=(array)
142
+ raise "you can only do this once" if @trackers_set
143
+ @trackers_set = true
144
+
145
+ array.each { |name| @config.register_tracker_name(name) }
146
+ end
147
+
148
+ def tracker(name)
149
+ @config.tracker_rails_config(name)
150
+ end
151
+ end
152
+
153
+ class << self
154
+ def singleton
155
+ @singleton ||= new
156
+ end
157
+ end
158
+ end
159
+ end