ariadna 1.0.0

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