ariadna 1.0.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.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ariadna.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,19 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
12
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
13
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
+ watch('config/routes.rb') { "spec/routing" }
15
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
+ # Capybara request specs
17
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
18
+ end
19
+
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Jorge Alvarez
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.
data/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # Ariadna
2
+
3
+ Google Analytics API wrapper.
4
+
5
+ It uses Oauth2 as authorization
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ gem 'ariadna'
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install ariadna
20
+
21
+ ## Usage
22
+
23
+ Create a new connexion with your Oauth2 access token
24
+
25
+ ```ruby
26
+ analytics = Ariadna::Analytics.new(access_token)
27
+ ```
28
+
29
+ Get a list of all accounts available
30
+
31
+ ```ruby
32
+ accounts = analytics.accounts
33
+ ```
34
+
35
+ Get a list of all web properties available for an account
36
+
37
+ ```ruby
38
+ properties = accounts.first.properties.all
39
+ ```
40
+
41
+ Get a list of all profiles available for a web property
42
+
43
+ ```ruby
44
+ profiles = properties.first.profiles.all
45
+ ```
46
+
47
+ Create a query with your metrics and dimensions
48
+
49
+ ```ruby
50
+ results = profile.results.select(
51
+ :metrics => [:visits, :bounces, :timeOnSite],
52
+ :dimensions => [:country]
53
+ )
54
+ .where(
55
+ :start_date => Date.today,
56
+ :end_date => 2.months.ago,
57
+ :browser => "==Firefox"
58
+ )
59
+ .limit(100)
60
+ .offset(40)
61
+ .order([:visits, :bounces])
62
+ .all
63
+ ```
64
+
65
+ All the metrics and dimensions returned by the query are mapped into attributes.
66
+
67
+ ```ruby
68
+ @results.each do |result|
69
+ puts result.visits
70
+ puts result.bounces
71
+ puts result.timeonsite
72
+ puts result.country
73
+ end
74
+ ```
75
+
76
+ ### Create a connexion
77
+
78
+ Ariadna::Analytics.new(access_token, proxy_settings, refresh_token_data)
79
+
80
+ There are three possible params:
81
+
82
+ access_token (mandatory): an oauth2 access token
83
+
84
+ proxy_settings (optional): a hash containing your proxy options
85
+
86
+ refresh_token_data (optional): a hash with information about your app so access_token can be renewed automatically in case it is expired.
87
+
88
+ ```ruby
89
+ analytics = Ariadna::Analytics.new(
90
+ access_token,
91
+ { proxy_host: 'proxy.yourproxy.com',
92
+ proxy_port: 8080,
93
+ proxy_user: 'username',
94
+ proxy_pass: 'password'
95
+ },
96
+ # Google access tokens are short term so chances are you are going to need to refresh them
97
+ { refresh_token: analytics_refresh_token,
98
+ client_id: 'apps.googleusercontent.com',
99
+ client_secret: 'client_secret',
100
+ current_user: current_user
101
+ }
102
+ )
103
+ ```
104
+
105
+ ### Access token
106
+
107
+ Ariadna is agnostic about the way you get your Oauth2 access token.
108
+
109
+ For the development of this gem I've been using [Omiauth](https://github.com/intridea/omniauth) with the [Google Oauth2 strategy](https://github.com/zquestz/omniauth-google-oauth2)
110
+
111
+ ```ruby
112
+ gem 'omniauth'
113
+
114
+ gem 'omniauth-google-oauth2'
115
+ ```
116
+
117
+ Google Oauth2 tokens have a very short life. To make things easy if the connexion gives a 401 error and there is a refresh token passed as a param Ariadna will try to get a new access token from Google and store it in the curren user info calling update_access_token_from_google. If you want to use this feature you must create a method in your user model that saves this token.
118
+
119
+ ```ruby
120
+ def update_access_token_from_google(new_token)
121
+ update_attribute(:google_oauth2_token, new_token)
122
+ end
123
+ ```
124
+
125
+ It is obviously out of the scope of this gem to update tokens but it is definetly something that will make your life easier.
126
+
127
+
128
+ ## Contributing
129
+
130
+ 1. Fork it
131
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
132
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
133
+ 4. Push to the branch (`git push origin my-new-feature`)
134
+ 5. Create new Pull Request
135
+
136
+ ## Contributors
137
+
138
+ * Jorge Alvarez [http://www.alvareznavarro.es](http://www.alvareznavarro.es/?utm_source=github&utm_medium=gem&utm_campaign=ariadna)
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/ariadna.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ariadna/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Jorge Alvarez"]
6
+ gem.email = ["jorge@alvareznavarro.es"]
7
+ gem.description = %q{Google Analytics A.P.I. V3 wrapper with oauth2}
8
+ gem.summary = %q{Google Analytics A.P.I. V3 wrapper with oauth2}
9
+ gem.homepage = "https://github.com/jorgegorka/ariadna"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "ariadna"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Ariadna::VERSION
17
+ gem.add_development_dependency "pry"
18
+ gem.add_development_dependency "rspec"
19
+ gem.add_development_dependency "guard-rspec"
20
+ end
@@ -0,0 +1,45 @@
1
+ module Ariadna
2
+ class Account
3
+
4
+ class << self;
5
+ attr_reader :url
6
+ attr_accessor :owner
7
+ end
8
+
9
+ #attr_reader :id, :link, :name, :properties_url
10
+
11
+ @url = "https://www.googleapis.com/analytics/v3/management/accounts"
12
+
13
+ def initialize(item)
14
+ item.each do |k,v|
15
+ instance_variable_set("@#{k}", v)
16
+ end
17
+ end
18
+
19
+ def self.all
20
+ @accounts ||= create_accounts
21
+ end
22
+
23
+ def properties
24
+ Delegator.new(WebProperty, self)
25
+ end
26
+
27
+ private
28
+
29
+ def self.create_accounts
30
+ accounts = Ariadna.connexion.get_url(self.url)
31
+ if (accounts["totalResults"].to_i > 0)
32
+ create_attributes(accounts["items"])
33
+ accounts["items"].map do |account|
34
+ Account.new(account)
35
+ end
36
+ end
37
+ end
38
+
39
+ def self.create_attributes(items)
40
+ items.first.each do |k,v|
41
+ attr_reader k.to_sym
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ module Ariadna
2
+ class Analytics
3
+ def initialize(token, proxy_options=nil, refresh_info=nil)
4
+ Ariadna.connexion = Connexion.new(token, proxy_options, refresh_info)
5
+ end
6
+
7
+ def accounts
8
+ Delegator.new(Account, self)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,118 @@
1
+ require 'net/http'
2
+
3
+ module Ariadna
4
+ class Connexion
5
+
6
+ def initialize(token, proxy_options, refresh_info)
7
+ @token = token
8
+ extract_proxy_options(proxy_options) if proxy_options
9
+ extract_refresh_info(refresh_info) if refresh_info
10
+ end
11
+
12
+ # get url and try to refresh token if Unauthorized
13
+ def get_url(url, params=nil)
14
+ uri = URI(url)
15
+ headers = Hash.new
16
+ resp = get_conn(uri)
17
+
18
+ case resp
19
+ # response is ok
20
+ when Net::HTTPSuccess, Net::HTTPRedirection
21
+ parse_response(resp)
22
+ # if we have a refresh token we can ask for a new access token
23
+ when Net::HTTPUnauthorized
24
+ get_url(url) if @refresh_token and get_access_token
25
+ when Net::HTTPBadRequest
26
+ raise Error.new(parse_response(resp))
27
+ when Net::HTTPNotFound
28
+ ["not found", uri]
29
+ else
30
+ resp.value
31
+ end
32
+
33
+ end
34
+
35
+ private
36
+
37
+ def extract_proxy_options(proxy_options)
38
+ return unless proxy_options.present?
39
+ @use_proxy = true
40
+ @proxy_host = proxy_options[:proxy_host]
41
+ @proxy_port = proxy_options[:proxy_port]
42
+ @proxy_user = proxy_options[:proxy_user]
43
+ @proxy_pass = proxy_options[:proxy_pass]
44
+ end
45
+
46
+ def extract_refresh_info(refresh_info)
47
+ return unless refresh_info.present?
48
+ @refresh_token = refresh_info[:refresh_token]
49
+ @client_id = refresh_info[:client_id]
50
+ @client_secret = refresh_info[:client_secret]
51
+ @current_user = refresh_info[:current_user]
52
+ end
53
+
54
+ # refresh access token as google access tokens have short term live
55
+ def get_access_token
56
+ uri = URI("https://accounts.google.com/o/oauth2/token")
57
+ req = Net::HTTP::Post.new(uri.request_uri)
58
+ req.set_form_data(
59
+ 'client_id' => @client_id,
60
+ 'client_secret' => @client_secret,
61
+ 'refresh_token' => @refresh_token,
62
+ 'grant_type' => 'refresh_token'
63
+ )
64
+ if @use_proxy
65
+ conn = Net::HTTP::Proxy(@proxy_host, @proxy_port, @proxy_user, @proxy_pass).start(uri.hostname, uri.port) {|http|
66
+ http.request(req)
67
+ }
68
+ else
69
+ # conn = Net::HTTP.new(uri.host, uri.port)
70
+ # conn.request(req)
71
+ http = Net::HTTP.new(uri.host, uri.port)
72
+ http.use_ssl = uri.port == 443
73
+ conn = http.start { |http| http.request(req) }
74
+ conn
75
+ end
76
+
77
+ case conn
78
+ # response is ok
79
+ when Net::HTTPSuccess, Net::HTTPRedirection
80
+ # use the new access token
81
+ refresh_info = parse_response(conn)
82
+ @token = refresh_info["access_token"]
83
+ @current_user.update_access_token_from_google(@token)
84
+ return true
85
+ # if not allowed revoke access
86
+ when Net::HTTPUnauthorized
87
+ @refresh_token = nil
88
+ else
89
+ conn
90
+ end
91
+ return false
92
+ end
93
+
94
+ def get_conn(uri)
95
+ req = Net::HTTP::Get.new(uri.request_uri)
96
+ req["Authorization"] = "Bearer #{@token}"
97
+ if @use_proxy
98
+ res = Net::HTTP::Proxy(@proxy_host, @proxy_port, @proxy_user, @proxy_pass).start(uri.hostname, uri.port) {|http|
99
+ http.request(req)
100
+ }
101
+ res
102
+ else
103
+ http = Net::HTTP.new(uri.host, uri.port)
104
+ http.use_ssl = uri.port == 443
105
+ conn = http.start {|http| http.request(req) }
106
+ conn
107
+ end
108
+ end
109
+
110
+ def parse_response(resp)
111
+ JSON.parse resp.body
112
+ end
113
+
114
+ def set_params(params)
115
+ {}
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,20 @@
1
+ module Ariadna
2
+ class Delegator < BasicObject
3
+
4
+ def initialize(target, owner)
5
+ @target = target
6
+ @target.owner = owner
7
+ end
8
+
9
+ protected
10
+
11
+ def method_missing(name, *args, &block)
12
+ target.send(name, *args, &block)
13
+ end
14
+
15
+ def target
16
+ @target ||= []
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ module Ariadna
2
+ class Error < StandardError
3
+ attr_reader :errors, :code, :message
4
+
5
+ def initialize(error_codes)
6
+ @errors = ErrorCode.get_errors(error_codes["error"]["errors"])
7
+ @code = error_codes["error"]["code"]
8
+ @message = error_codes["error"]["message"]
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module Ariadna
2
+ class ErrorCode
3
+ def initialize(item)
4
+ item.each do |k,v|
5
+ instance_variable_set("@#{k}", v)
6
+ end
7
+ end
8
+
9
+ def self.get_errors(errors)
10
+ create_attributes(errors.first)
11
+ errors.map do |item|
12
+ ErrorCode.new(item)
13
+ end
14
+ end
15
+
16
+ def self.create_attributes(item)
17
+ item.each do |k,v|
18
+ attr_reader k.to_sym
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ module Ariadna
2
+ class Profile
3
+
4
+ class << self;
5
+ attr_accessor :owner
6
+ end
7
+
8
+ attr_reader :id, :link, :name, :goals, :parent
9
+
10
+ def initialize(item)
11
+ item.each do |k,v|
12
+ instance_variable_set("@#{k}", v)
13
+ end
14
+ end
15
+
16
+ def self.all
17
+ @profiles ||= create_profiles
18
+ end
19
+
20
+ def results
21
+ Delegator.new(Result, self)
22
+ end
23
+
24
+ private
25
+
26
+ def self.create_profiles
27
+ profiles = Ariadna.connexion.get_url(@owner.childLink["href"])
28
+ if (profiles["totalResults"].to_i > 0)
29
+ create_attributes(profiles["items"])
30
+ profiles["items"].map do |item|
31
+ Profile.new(item)
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.create_attributes(items)
37
+ items.first.each do |k,v|
38
+ attr_reader k.to_sym
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,167 @@
1
+ module Ariadna
2
+ class Result
3
+
4
+ class << self
5
+ attr_accessor :owner
6
+ attr_accessor :url
7
+ end
8
+
9
+ URL = "https://www.googleapis.com/analytics/v3/data/ga"
10
+
11
+ #gel all results
12
+ def self.all
13
+ get_results
14
+ end
15
+
16
+ # metrics and dimensions
17
+
18
+ def self.select(params)
19
+ get_metrics_and_dimensions(params)
20
+ self
21
+ end
22
+
23
+ # main filter conditions
24
+ def self.where(params)
25
+ extract_dates(params)
26
+ get_filters(params) unless params.empty?
27
+ self
28
+ end
29
+
30
+ # sort conditions for the query
31
+ def self.order(params)
32
+ conditions.merge!({"sort" => api_compatible_names(params)})
33
+ self
34
+ end
35
+
36
+ # number of results returned
37
+ def self.limit(results)
38
+ if ((results.to_i > 0) and (results.to_i < 1001))
39
+ conditions.merge!({"max-results" => results.to_i})
40
+ end
41
+ self
42
+ end
43
+
44
+ # number of row from which to start collecting results (used for pagination)
45
+ def self.offset(offset)
46
+ if (offset.to_i > 0)
47
+ conditions.merge!({"start-index" => offset.to_i})
48
+ end
49
+ self
50
+ end
51
+
52
+ # lazy load query. Only executed when actually needed
53
+ def self.each(&block)
54
+ get_results.each(&block)
55
+ end
56
+
57
+ private
58
+
59
+ def self.conditions
60
+ @conditions ||= {}
61
+ end
62
+
63
+ def self.accessor_name(header)
64
+ header["name"].sub('ga:', '')
65
+ end
66
+
67
+ # create attributes for each metric and dimension
68
+ def self.create_attributes(results)
69
+ summary_rows = Hash.new
70
+ summary_rows.merge!(results)
71
+ summary_rows.delete("columnHeaders")
72
+ summary_rows.delete("rows")
73
+ summary_rows.each do |row, value|
74
+ attr_reader row.to_sym
75
+ end
76
+ summary_rows
77
+ end
78
+
79
+ # create attributes for each metric and dimension
80
+ def self.create_metrics_and_dimensions(headers)
81
+ headers.each do |header|
82
+ attr_reader accessor_name(header).to_sym
83
+ end
84
+ end
85
+
86
+ # map the json results collection into result objects
87
+ # every metric and dimension is created as an attribute
88
+ # I.E. You can get result.visits or result.bounces
89
+ def self.get_results
90
+ self.url = generate_url
91
+ results = Ariadna.connexion.get_url(self.url)
92
+
93
+ return results unless results.is_a? Hash
94
+
95
+ if (results["totalResults"].to_i > 0)
96
+ #create an accessor for each summary attribute
97
+ summary_rows = create_attributes(results)
98
+ #create an accessor for each metric and dimension
99
+ create_metrics_and_dimensions(results["columnHeaders"])
100
+ results["rows"].map do |items|
101
+ res = Result.new
102
+ #assign values to summary fields
103
+ summary_rows.each do |name, value|
104
+ res.instance_variable_set("@#{name}", value)
105
+ end
106
+ #assign values to metrics and dimensions
107
+ items.each do |item|
108
+ res.instance_variable_set("@#{accessor_name(results["columnHeaders"][(items.index(item))])}", set_value_for_result(results["columnHeaders"][(items.index(item))], item))
109
+ end
110
+ res
111
+ end
112
+ end
113
+ end
114
+
115
+ def self.set_value_for_result(header, item)
116
+ case header["dataType"]
117
+ when "INTEGER"
118
+ return item.to_i
119
+ when "CURRENCY"
120
+ return item.to_d
121
+ when "FLOAT"
122
+ return item.to_f
123
+ when "TIME"
124
+ Time.at(item.to_d).gmtime.strftime('%R:%S')
125
+ else
126
+ return item.to_s
127
+ end
128
+ end
129
+
130
+ def self.generate_url
131
+ params = conditions.merge({"ids" => "ga:#{@owner.id}"})
132
+ "#{URL}?" + params.map{ |k,v| "#{k}=#{v}"}.join("&")
133
+ end
134
+
135
+ def self.get_filters(params)
136
+ filters = params.map do |k,v|
137
+ "#{api_compatible_names([k])}#{url_encoded_value(v)}"
138
+ end
139
+ conditions.merge!({"filters" => filters.join(",")})
140
+ end
141
+
142
+ def self.get_metrics_and_dimensions(params)
143
+ params.each do |k,v|
144
+ conditions.merge!({"#{k}" => api_compatible_names(v)})
145
+ end
146
+ end
147
+
148
+ def self.api_compatible_names(values)
149
+ values.collect {|e| "ga:#{e}"}.join(",")
150
+ end
151
+
152
+ def self.extract_dates(params)
153
+ start_date = params.delete(:start_date)
154
+ end_date = params.delete(:end_date)
155
+ conditions.merge!({"start-date" => format_date(start_date)})
156
+ conditions.merge!({"end-date" => format_date(end_date)})
157
+ end
158
+
159
+ def self.format_date(date)
160
+ date.strftime("%Y-%m-%d")
161
+ end
162
+
163
+ def self.url_encoded_value(value)
164
+ URI.escape(value, "=@!><")
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,3 @@
1
+ module Ariadna
2
+ VERSION = "1.0.0"
3
+ end