1

I am looking to understand how I can access a variable set in method A, then use that variable in method B, and also a clean way of reusing the same part of code and then only changing the query

require 'google/api_client'

module GoogleAnalytics
 class Analytic

 SERVICE_ACCOUNT_EMAIL_ADDRESS = ENV['SERVICE_ACCOUNT_EMAIL_ADDRESS']
 PATH_TO_KEY_FILE              = ENV['PATH_TO_KEY_FILE']
 PROFILE                       = ENV['ANALYTICS_PROFILE_ID']

def google_analytics_api

client  = Google::APIClient.new(
  application_name: "Example Application",
  application_version: "1.0")

client.authorization = Signet::OAuth2::Client.new(
  :token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
  :audience             => 'https://accounts.google.com/o/oauth2/token',
  :scope                => 'https://www.googleapis.com/auth/analytics.readonly',
  :issuer               => SERVICE_ACCOUNT_EMAIL_ADDRESS,
  :signing_key          => Google::APIClient::KeyUtils.load_from_pkcs12(PATH_TO_KEY_FILE, 'notasecret')).tap { |auth| auth.fetch_access_token! }

  api_method = client.discovered_api('analytics','v3').data.ga.get


  # make queries
  result = client.execute(:api_method => api_method, :parameters => {
  'ids'        => PROFILE,
  'start-date' => Date.new(2014,1,1).to_s,
  'end-date'   => Date.today.to_s,
  'dimensions' => 'ga:pagePath',
  'metrics'    => 'ga:pageviews',
  'filters'    => 'ga:pagePath==/'
 })
  end
 end
end

So if i run the method google_analytics_api i get a set of results returned assigned to the variable result.

So what if i want another 2 separate methods that will return different sets of results, so new users and bounce rates, that would be two separate calls changing the request params wouldnt it? would i have to repeat the whole method?

Is there a way to refactor this so that the authorization call can be wrapped up in its on method and all i change is the request params assigned to result ?

So something like this

def api_call
  logic to make request
end

def new_users
  api_call
   # make queries
  result = client.execute(:api_method => api_method, :parameters => {
  'ids'        => PROFILE,
  'start-date' => Date.new(2014,1,1).to_s,
  'end-date'   => Date.today.to_s,
  'dimensions' => 'ga:pagePath',
  'metrics'    => 'ga:newUsers',
  'filters'    => 'ga:pagePath==/'
 })


end

One of the problems though will be having the local variables client and result available in the new_users method, what could these be changed to? an instance variable with an @? or a class variable with an @@ ?

Richlewis
  • 15,070
  • 37
  • 122
  • 283

2 Answers2

4

Your instincts are good - you don't want to repeat yourself, and there are better ways of structuring this code. But rather than sharing variables, you should think about small pieces, loosely joined. Write methods that do one thing well, and combine them together. For instance, we could write a get_client method that just returns a client for other methods to use:

protected
def get_client
  client  = Google::APIClient.new(
    application_name: "Example Application",
    application_version: "1.0")

  client.authorization = Signet::OAuth2::Client.new(
    :token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
    :audience             => 'https://accounts.google.com/o/oauth2/token',
    :scope                => 'https://www.googleapis.com/auth/analytics.readonly',
    :issuer               => SERVICE_ACCOUNT_EMAIL_ADDRESS,
    :signing_key          => Google::APIClient::KeyUtils.load_from_pkcs12(PATH_TO_KEY_FILE, 'notasecret')).tap { |auth| auth.fetch_access_token! }
  client
end

It's protected because external code - stuff outside your Analytic class - shouldn't work with it directly. They should use the methods we provide for them.


You could also provide a helper method for getting results from the API. I'm not familiar with the query API, but it looks like it's your metrics value that changes. So maybe something like this:

protected
def get_result(metrics)
  client = self.get_client
  api_method = client.discovered_api('analytics','v3').data.ga.get

  result = client.execute(:api_method => api_method, :parameters => {
    'ids'        => PROFILE,
    'start-date' => Date.new(2014,1,1).to_s,
    'end-date'   => Date.today.to_s,
    'dimensions' => 'ga:pagePath',
    'metrics'    => metrics,
    'filters'    => 'ga:pagePath==/'
   })
  result
end

Now you can write simple methods that your external classes can use:

def new_users
  get_result('ga:newUsers')
end

def total_visits
  get_result('ga:pageViews')
end

If you can, try to return simple data from these methods. Maybe total_visits is going to return get_result('ga:pageViews')['totalsForAllResults']['ga:pageviews'] instead. Code outside your class shouldn't have to know about the GA data format to work with it. This is called encapsulation.

Alex P
  • 5,942
  • 2
  • 23
  • 30
  • thats a great answer, explained well, though one thing im unsure of is the call in get_result..... client = self.get_client...what is this doing? I have only ever used self in a model class – Richlewis Jul 23 '14 at 09:24
  • 1
    It's just invoking the `get_client` method, which returns a `client` for the method to use. You could write that line as `client = get_client` or `client = get_client()` too. – Alex P Jul 23 '14 at 09:28
  • thank you very much, always great when you can learn a lot from an answer – Richlewis Jul 23 '14 at 09:34
2

From talking on Skype, I think there are several things to look at


Init

Currently, you're using the google_analytics_api method every time you want to use the module. This is completely inefficient, and is partly why you have this issue now. Instead, I would create an init method, which will fire each time you initialize the object (and make GoogleAnalytics into a class of its own):

#lib/google_analytics.rb
Class GoogleAnalytics
   def initialize
       ... google_analytics_api method here
   end
end

This will allow you to treat your current module as a real Ruby object - like this:

@analytics = GoogleAnalytics.new #-> fires initialize method

This will give you the ability to call the object (which will pull the data from the API), and then split that data accordingly, for the different use cases you have.


Instance Methods

This leads me nicely onto the idea of instance methods

What you're referring to, and indeed what Alex P is referring to, is the idea of an instance method. This doubles as an attribute for an object, but essentially allows you to call a piece of functionality on an instance of a method.

So in Alex's example, you have:

def new_users
  get_result('ga:newUsers')
end

This is just calling an instance method of your class:

GoogleAnalytics::Analytic.new_users

This will create an instance of the Analytic class, and then call the new_users method (which should be a class method). This method will then allow you to call instance methods on the newly initialized object, hence the get_result method call

--

What I'm proposing is to use instance methods after the object has been initialized, giving you acesss to the data defined with google_analytics_api

For example:

#app/controllers/analyics_controller.rb
Class AnalyticsController < ApplicationController
   def index
       @analytics = GoogleAnalytics.new
       @new_users = @analytics.new_users
   end
end

#lib/google_analytics.rb
Class GoogleAnalytics
   def initialize
       ... google_analytics_api method here
   end

   def new_users
      return [new_users_data]
   end
end

The one caveat to this is whether this will work without the module. I think it should, but it's untested for me

Community
  • 1
  • 1
Richard Peck
  • 76,116
  • 9
  • 93
  • 147