gaq 0.2.0

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