gxapi_rails 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +161 -0
  4. data/Rakefile +25 -0
  5. data/app/helpers/gxapi_helper.rb +108 -0
  6. data/lib/gxapi.rb +160 -0
  7. data/lib/gxapi/base.rb +100 -0
  8. data/lib/gxapi/controller_methods.rb +41 -0
  9. data/lib/gxapi/engine.rb +14 -0
  10. data/lib/gxapi/google_analytics.rb +193 -0
  11. data/lib/gxapi/ostruct.rb +86 -0
  12. data/lib/gxapi/version.rb +3 -0
  13. data/spec/dummy/README.rdoc +261 -0
  14. data/spec/dummy/Rakefile +7 -0
  15. data/spec/dummy/app/assets/javascripts/application.js +13 -0
  16. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  17. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  18. data/spec/dummy/app/controllers/posts_controller.rb +11 -0
  19. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  20. data/spec/dummy/app/helpers/posts_helper.rb +2 -0
  21. data/spec/dummy/app/models/post.rb +3 -0
  22. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  23. data/spec/dummy/app/views/posts/_form.html.erb +25 -0
  24. data/spec/dummy/app/views/posts/edit.html.erb +6 -0
  25. data/spec/dummy/app/views/posts/index.html.erb +36 -0
  26. data/spec/dummy/app/views/posts/new.html.erb +5 -0
  27. data/spec/dummy/app/views/posts/show.html.erb +15 -0
  28. data/spec/dummy/config.ru +4 -0
  29. data/spec/dummy/config/application.rb +70 -0
  30. data/spec/dummy/config/boot.rb +10 -0
  31. data/spec/dummy/config/database.yml +22 -0
  32. data/spec/dummy/config/database.yml.default +111 -0
  33. data/spec/dummy/config/environment.rb +5 -0
  34. data/spec/dummy/config/environments/development.rb +38 -0
  35. data/spec/dummy/config/environments/production.rb +67 -0
  36. data/spec/dummy/config/environments/test.rb +37 -0
  37. data/spec/dummy/config/gxapi.yml +17 -0
  38. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  39. data/spec/dummy/config/initializers/inflections.rb +15 -0
  40. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  41. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  42. data/spec/dummy/config/initializers/session_store.rb +8 -0
  43. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  44. data/spec/dummy/config/konfig/application.rb +16 -0
  45. data/spec/dummy/config/konfig/development.rb +10 -0
  46. data/spec/dummy/config/konfig/production.rb +3 -0
  47. data/spec/dummy/config/konfig/test.rb +3 -0
  48. data/spec/dummy/config/konfig/test_prod.rb +3 -0
  49. data/spec/dummy/config/locales/en.yml +5 -0
  50. data/spec/dummy/config/routes.rb +3 -0
  51. data/spec/dummy/config/test.p12 +0 -0
  52. data/spec/dummy/data/log/dummy/development.log +113 -0
  53. data/spec/dummy/data/log/dummy/test.log +927 -0
  54. data/spec/dummy/db/development.sqlite3 +0 -0
  55. data/spec/dummy/db/test.sqlite3 +0 -0
  56. data/spec/dummy/log/test.log +1443 -0
  57. data/spec/dummy/public/404.html +26 -0
  58. data/spec/dummy/public/422.html +26 -0
  59. data/spec/dummy/public/500.html +25 -0
  60. data/spec/dummy/public/favicon.ico +0 -0
  61. data/spec/dummy/script/rails +6 -0
  62. data/spec/features/display_variant_js_spec.rb +34 -0
  63. data/spec/helpers/gxapi_helper_spec.rb +63 -0
  64. data/spec/lib/gxapi/base_spec.rb +80 -0
  65. data/spec/lib/gxapi/google_analytics_spec.rb +70 -0
  66. data/spec/lib/gxapi_spec.rb +32 -0
  67. data/spec/spec_helper.rb +32 -0
  68. metadata +233 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1fc14fc4272595a73becebfd7d22821d6c0f90af
4
+ data.tar.gz: cb8ec33f3f319d597e91666bf3a14301374d07d3
5
+ SHA512:
6
+ metadata.gz: 1d787804b60b6a832378ba4ad5cfeccdda4126a49cac129c26986abb98a6fcad44fe46d2c98ae22a47be8e9f6c118cb4e5bf26625df99a50fad9612ec35e34a1
7
+ data.tar.gz: 061edc4dadaf492b59137b5660df80aaa828a75db68c4f633a7a8c74a72c1168f4847b5355718842b352ea556081fc9fb5b0d93165b14c738f368988ae32e929
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 Dan Langevin
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # Gxapi
2
+
3
+ [![Code Climate](https://codeclimate.com/repos/5276603656b10215e9014c80/badges/ddae4ab98746f66abf45/gpa.png)](https://codeclimate.com/repos/5276603656b10215e9014c80/feed) [![Build Status](https://travis-ci.org/dlangevin/gxapi_rails.png?branch=master)](https://travis-ci.org/dlangevin/gxapi_rails)
4
+
5
+ Gxapi interfaces with Google Analytics Experiments to retrieve data from
6
+ its API and determine which variant should be presented to a given user
7
+
8
+ ## Installation
9
+ % gem install gxapi_rails
10
+ % rails g gxapi_rails
11
+
12
+ ## Configuration
13
+
14
+ First, you must create a Google Analytics Service Account.
15
+ Information can be found here https://developers.google.com/accounts/docs/OAuth2ServiceAccount
16
+
17
+ Gxapi uses `#{Rails.root}/config/gxapi.yml` to configure variables. You can
18
+ use different configurations per environment (development/test/production)
19
+
20
+ ### Example Configuration
21
+ development:
22
+ google_analytics:
23
+ account_id: ACCOUNT_ID
24
+ profile_id: PROFILE_ID
25
+ web_property_id: WEB_PROPERTY_ID
26
+ google:
27
+ email: SERVICE_ACCOUNT_EMAIL
28
+ private_key_path: 'PATH_TO_SERVICE_ACCOUNT_PRIVATE_KEY'
29
+
30
+ ### Where the F do I find all this stuff?
31
+
32
+ <a href="https://developers.google.com/accounts/docs/OAuth2ServiceAccount"
33
+ target="_blank">Some instructions</a>
34
+ on creating a Google Analytics Service Account
35
+
36
+ <a href="https://developers.google.com/analytics/resources/concepts/gaConceptsAccounts"
37
+ target="_blank">Some instructions</a> on where to find your
38
+ Google Analytics settings
39
+
40
+
41
+ ## Usage
42
+
43
+ ### Layout
44
+
45
+ First, add the experiment Javascript in your layout
46
+
47
+ # app/views/layouts/application.html.erb
48
+ <%= gxapi_experiment_js %>
49
+
50
+ This will render the Javascript tag to pull in the Google Analytics
51
+ Content Experiment JS source and the corresponding call to send your
52
+ experiment data in. If you are specifying a custom variable name
53
+ (the default is 'variant'), you can add it here as well
54
+
55
+ # app/views/layouts/application.html.erb
56
+ <%= gxapi_experiment_js(:custom_var) %>
57
+
58
+ If you have multiple experiments on the same page, you can add this same
59
+ call for each experiment in a view
60
+
61
+ # app/views/layouts/application.html.erb
62
+ <%= gxapi_experiment_js %>
63
+
64
+ # app/views/posts/index.html.erb
65
+ <%= gxapi_experiment_js(:other_experiment_key) %>
66
+
67
+ ### Controller
68
+
69
+ Once you have set up an experiment in Google Analytics, you can reference
70
+ it by name in your controller
71
+
72
+ class PostsController < ApplicationController
73
+
74
+ def index
75
+ gxapi_get_variant("My Experiment")
76
+ end
77
+
78
+ end
79
+
80
+ This will use the default key `variant` for the experiment. To customize
81
+ this name, you can pass another value. This is useful when you are using
82
+ multiple experiments on the same page
83
+
84
+ class PostsController < ApplicationController
85
+
86
+ def index
87
+ gxapi_get_variant("My Experiment", :exp1)
88
+ gxapi_get_variant("Other Experiment", :exp2)
89
+ end
90
+
91
+ end
92
+
93
+ ### Control Flow
94
+
95
+ In the controller and the view, you can access the variant name to determine
96
+ which path your code should take. `gxapi_get_variant` returns a
97
+ `Celluloid::Future` to prevent blocking while the result is calculated. To
98
+ get the value in the controller you can call `#value`. In the view,
99
+ Gxapi provides a the `gxapi_variant_name` helper.
100
+
101
+ class PostsController < ApplicationController
102
+
103
+ def index
104
+ gxapi_get_variant("My Experiment", :exp1)
105
+ end
106
+
107
+ end
108
+
109
+ # app/views/posts/index.html.erb
110
+
111
+ <h1>My Page</h1>
112
+
113
+ <% if gxapi_variant_name(:exp1) == 'version_a' -%>
114
+ Do some stuff...
115
+ <% else -%>
116
+ Do some other stuff...
117
+ <% end -%>
118
+
119
+ ### Testing and overriding
120
+
121
+ In order to view a specific version, you can pass `experiment_name=value`
122
+ into a GET request to the action. This will short-circuit the rendering
123
+ of the JavaScript call to Google so testing will not affect your results
124
+
125
+ # GET /posts.html?variant=my_var
126
+
127
+ # app/controllers/posts_controller.rb
128
+ def index
129
+ gxapi_get_variant("My Experiment")
130
+ end
131
+
132
+ # app/views/posts/index.html.erb
133
+ gxapi_variant_name # Will always equal 'my_var'
134
+
135
+
136
+ # GET /posts.html?custom_name=my_var
137
+
138
+ # app/controllers/posts_controller.rb
139
+ def index
140
+ gxapi_get_variant("My Experiment", :custom_name)
141
+ end
142
+
143
+ # app/views/posts/index.html.erb
144
+ gxapi_variant_name(:custom_name) # Will always equal 'my_var'
145
+
146
+
147
+ ## Contributing to gxapi
148
+
149
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
150
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
151
+ * Fork the project.
152
+ * Start a feature/bugfix branch.
153
+ * Commit and push until you are happy with your contribution.
154
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
155
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
156
+
157
+ ## Copyright
158
+
159
+ Copyright (c) 2013 Dan Langevin. See LICENSE.txt for
160
+ further details.
161
+
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'rspec/core'
15
+ require 'rspec/core/rake_task'
16
+ RSpec::Core::RakeTask.new(:spec) do |spec|
17
+ spec.pattern = FileList['spec/**/*_spec.rb']
18
+ end
19
+
20
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
21
+ spec.pattern = 'spec/**/*_spec.rb'
22
+ spec.rcov = true
23
+ end
24
+
25
+ task :default => :spec
@@ -0,0 +1,108 @@
1
+
2
+ #
3
+ # View helper for Gxapi.
4
+ #
5
+ # @author [dlangevin]
6
+ #
7
+ module GxapiHelper
8
+
9
+ #
10
+ # Get the HTMl to load script from Google Ananlytics
11
+ # and setChosenVariation to tell it which one we served
12
+ #
13
+ # @return [String] HTML
14
+ def gxapi_experiment_js(*args)
15
+ # extract options
16
+ opts = args.last.is_a?(Hash) ? args.pop : {}
17
+ # default
18
+ ivar_name = args.first || :variant
19
+
20
+ unless variant = self.get_variant(ivar_name)
21
+ return ""
22
+ end
23
+
24
+ # our return value
25
+ ret = ""
26
+ # first time we call this, we add the script
27
+ unless @gxapi_experiment_called == true
28
+ ret += self.gxapi_source(opts)
29
+ Gxapi.logger.debug {"Rendered Gxapi source"}
30
+ end
31
+
32
+ @gxapi_experiment_called = true
33
+ ret += self.get_variant_js(variant)
34
+
35
+ Gxapi.logger.debug { "#{ivar_name} is now #{variant.value.name}" }
36
+
37
+ ret.html_safe
38
+ end
39
+
40
+ #
41
+ # Get the version for a given variant
42
+ # @return [String]
43
+ def gxapi_variant_name(ivar_name = :variant)
44
+ # if we have params[ivar], we just use it
45
+ return "default" unless variant = instance_variable_get("@#{ivar_name}")
46
+ return variant.value.try(:name) || "default"
47
+ end
48
+
49
+ protected
50
+
51
+ #
52
+ # Get the variant if it exists and has an experiment_id
53
+ #
54
+ # @param ivar_name [String] Name of the instance variable for the
55
+ # variant
56
+ #
57
+ # @return [Gxapi::Ostruct, false] Variant if it exists, false otherwise
58
+ def get_variant(ivar_name)
59
+ # make sure we have our variant
60
+ unless variant = instance_variable_get("@#{ivar_name}")
61
+ Gxapi.logger.debug { "No variant found - #{ivar_name}" }
62
+ return false
63
+ end
64
+ # and a valid experiment id
65
+ unless variant.value.experiment_id.present?
66
+ Gxapi.logger.debug { "No experiment_id found : #{variant.value} "}
67
+ return false
68
+ end
69
+ return variant
70
+ end
71
+
72
+ #
73
+ # Get the JS for a variant
74
+ #
75
+ # @param variant [Ostruct] Variant definition
76
+ #
77
+ # @return [String] JS to render
78
+ def get_variant_js(variant)
79
+ javascript_tag{
80
+ "cxApi.setChosenVariation(
81
+ #{variant.value.index},
82
+ '#{escape_javascript(variant.value.experiment_id)}'
83
+ );".html_safe
84
+ }
85
+ end
86
+
87
+ #
88
+ # The Javascript tag for the Content experiment API
89
+ #
90
+ # @param [Hash] opts Hash of options
91
+ #
92
+ # @return [String] HTML for the tag
93
+ def gxapi_source(opts)
94
+ ret = javascript_include_tag(
95
+ "https://www.google-analytics.com/cx/api.js"
96
+ )
97
+ ret += "\n"
98
+ # set domain name if present
99
+ if opts[:domain]
100
+ domain_call = javascript_tag{
101
+ "cxApi.setDomainName('#{escape_javascript(opts[:domain])}');".html_safe
102
+ }
103
+ ret += domain_call.html_safe
104
+ ret += "\n"
105
+ end
106
+ ret
107
+ end
108
+ end
data/lib/gxapi.rb ADDED
@@ -0,0 +1,160 @@
1
+ require 'active_support'
2
+ require 'celluloid'
3
+ require 'erb'
4
+ require 'json'
5
+ require 'yaml'
6
+
7
+ require File.expand_path('../gxapi/base', __FILE__)
8
+ require File.expand_path('../gxapi/google_analytics', __FILE__)
9
+ require File.expand_path('../gxapi/ostruct', __FILE__)
10
+ require File.expand_path('../gxapi/version', __FILE__)
11
+
12
+
13
+ if defined?(::Rails)
14
+ require File.expand_path('../gxapi/controller_methods', __FILE__)
15
+ require File.expand_path('../gxapi/engine', __FILE__)
16
+ end
17
+
18
+ module Gxapi
19
+
20
+ #
21
+ # get our cache instance
22
+ #
23
+ # @return [ActiveSupport::Cache::Store]
24
+ def self.cache
25
+ # if we have an overridden cache, return it
26
+ return @overridden_cache if defined?(@overridden_cache)
27
+ # use Rails.cache if it is defined
28
+ return ::Rails.cache if defined?(::Rails) && ::Rails.cache
29
+ # last resort, just use our own cache choice
30
+ @cache ||= ActiveSupport::Cache::MemoryStore.new
31
+ end
32
+
33
+ #
34
+ # setter for {Gxapi.cache}
35
+ #
36
+ # @return [ActiveSupport::Cache::Store] The new cache object
37
+ def self.cache=(cache)
38
+ @overridden_cache = cache
39
+ end
40
+
41
+ #
42
+ # namespace for our cache keys
43
+ #
44
+ # @return [String, nil]
45
+ def self.cache_namespace
46
+ @cache_namespace
47
+ end
48
+
49
+ #
50
+ # setter for {Gxapi.cache_namespace}
51
+ #
52
+ # @return [String] New value for cache_namespace
53
+ def self.cache_namespace=(val)
54
+ @cache_namespace = val
55
+ end
56
+
57
+ #
58
+ # Gxapi config - this is loaded based on the
59
+ # {Gxapi.config_path}
60
+ #
61
+ # @return [Gxapi::Ostruct]
62
+ def self.config
63
+ @config ||= begin
64
+ # parse our yml file after running it through ERB
65
+ contents = File.read(self.config_path)
66
+ yml = ERB.new(contents).result(binding)
67
+ Gxapi::Ostruct.new(YAML.load(yml)[Gxapi.env])
68
+ end
69
+ end
70
+
71
+ #
72
+ # get the config path for our config YAML file
73
+ #
74
+ # @return [String] defaults to #{Rails.root}/config/gxapi.yml
75
+ def self.config_path
76
+ @config_path ||= File.join(Rails.root, "config/gxapi.yml")
77
+ end
78
+
79
+ #
80
+ # setter for config path
81
+ #
82
+ # @return [String] value of {Gxapi.config_path}
83
+ def self.config_path=(val)
84
+ @config_path = val
85
+ end
86
+
87
+ #
88
+ # our environment - defaults to Rails.env or test
89
+ #
90
+ # @return [String]
91
+ def self.env
92
+ @env ||= defined?(::Rails) ? ::Rails.env : "test"
93
+ end
94
+
95
+ #
96
+ # Set the value of {Gxapi.env}
97
+ #
98
+ # @return [String] environment
99
+ def self.env=(val)
100
+ @env = val
101
+ end
102
+
103
+ #
104
+ # instance of logger for Gxapi
105
+ #
106
+ # @return [Logger, Log4r::Logger]
107
+ def self.logger
108
+ @logger ||= begin
109
+ if defined?(::Rails) && ::Rails.logger
110
+ ::Rails.logger
111
+ else
112
+ Logger.new(STDOUT)
113
+ end
114
+ end
115
+ end
116
+
117
+
118
+ #
119
+ # Setter for the Logger
120
+ # @param logger [Logger, Log4r]
121
+ #
122
+ # @return [Logger, Log4r] Logger instance
123
+ def self.logger=(logger)
124
+ @logger = logger
125
+ end
126
+
127
+ #
128
+ # Reload all data from experiments
129
+ #
130
+ # @return [Boolean] Always true
131
+ def self.reload_experiments
132
+ Base.new("").reload_experiments
133
+ true
134
+ end
135
+
136
+
137
+ #
138
+ # root directory for gxapi
139
+ #
140
+ # @return [String]
141
+ def self.root
142
+ File.expand_path("../../", __FILE__)
143
+ end
144
+
145
+ #
146
+ # Wrap with error handling
147
+ # logs errors and returns false if an error
148
+ # occurs
149
+ #
150
+ # @return [Value, false]
151
+ def self.with_error_handling(&block)
152
+ begin
153
+ yield
154
+ rescue => e
155
+ self.logger.error(e.message)
156
+ self.logger.error(e.backtrace)
157
+ false
158
+ end
159
+ end
160
+ end