December

19

ColdFusion and Google Analytics: Getting Out What You Put In

The Hooking into Google Analytics post details how to connect to the Data Export API of Google Analytics (GA) and get a simple visitors count in return. Here we will expand greatly on that and create a cfc that will process just about any result from the API. I submitted this topic to present at CFUnited but haven’t heard back from them yet. Here’s hoping.

An auth token is needed for requests to the API and two ways to do that are the AuthSub and ClentLogin methods. The method used is up to the developer as once the auth token is retrieved and set into session, data calls to the API can be repeatedly made.

<cffunction name="googleLogin" access="public" hint="GA account authorization">
        <cfargument name="email" type="string" required="yes" default="">
        <cfargument name="password" type="string"required="yes" default="">
        <cfargument name="gaLoginUrl" type="string" required="no" default="https://www.google.com/accounts/ClientLogin">

        <cfset var loginAuth = "" />

        <cfhttp url="#arguments.gaLoginUrl#" method="post">
            <cfhttpparam name="accountType" type="url" value="GOOGLE">
            <cfhttpparam name="Email" type="url" value="#arguments.email#">
            <cfhttpparam name="Passwd" type="url" value="#arguments.password#">
            <cfhttpparam name="service" type="url" value="analytics">
            <cfhttpparam name="source" type="url" value="my-analytics">
        </cfhttp>

        <cfif NOT FindNoCase("Auth=",cfhttp.filecontent)>
            <cfset loginAuth = "Authorization Failed" />
        <cfelse>
            <cfset loginAuth = Mid(cfhttp.filecontent, FindNoCase("Auth=",cfhttp.filecontent) + (Len("Auth=")), Len(cfhttp.filecontent)) />
        </cfif>

         <cflock scope="session" type="exclusive" timeout="5">
                <cfset session.ga_loginAuth = loginAuth />
         </cflock>

    </cffunction>

To retrieve the profile or data feed, a call is made to the API via a function.

    <cffunction name="callApi" access="public" returntype="array" hint="GA data as array of structures">
        <cfargument name="gaUrl" type="string" required="yes">
        <cfargument name="authToken" type="string" required="yes">

        <cfset var authTokenHeader = 'GoogleLogin auth=' & arguments.authToken />
        <cfset var responseOutput = "" />

        <cfhttp url="#arguments.gaUrl#" method="get">
            <cfhttpparam name="Authorization" type="header" value="#authTokenHeader#">
        </cfhttp>
        <cfset responseOutput = cfhttp.filecontent />

        <!---remove dxp: prefix from nodes that have it and strip xmlns from feed element--->
         <cfset responseOutput = responseOutput.ReplaceAll("(</?)(\w+:)","$1") />
         <cfset responseOutput = REReplaceNoCase(responseOutput,"<feed[^>]*>","<feed>") />

         <!---entry nodes hold the data--->
         <cfset entryNodes = XmlSearch(responseOutput, '//entry/') />

         <cfreturn entryNodes />
    </cffunction>

Next, a list of the profiles (websites) that the user has access to are retrieved by requesting an account feed. If the user has only one profile associated with their credentials, the data is displayed immediately. If they have more than one, they are presented with a select box to select the profile they want data from.

<cfinvoke component="ga" method="parseProfiles" returnvariable="profilesArray">
          <cfinvokeargument name="gaUrl" value="https://www.google.com/analytics/feeds/accounts/default" />
          <cfinvokeargument name="authToken" value="#session.ga_loginAuth#" />
</cfinvoke>
<cffunction name="parseProfiles" access="public" returntype="array" hint="GA profiles as array of structures">
        <cfargument name="gaUrl" type="string" required="yes">
        <cfargument name="authToken" type="string" required="yes">

        <cfset var profileArray = ArrayNew(1) />
        <cfset var entryStruct = StructNew() />

        <cfset entryNodes = callApi(arguments.gaUrl,arguments.authToken) />

        <cfloop from="1" to="#ArrayLen(entryNodes)#" index="num">
            <cfset entryStruct = StructNew() />

            <cfset entryStruct.id = entryNodes[num].id.XmlText />
            <cfset entryStruct.title = entryNodes[num].title.XmlText />
            <cfset entryStruct.tableId = entryNodes[num].tableId.XmlText />

            <cfloop from="1" to="#ArrayLen(entryNodes[num].property)#" index="i">
                <cfswitch expression='#entryNodes[num].property[i].XmlAttributes["name"]#'>
                    <cfcase value="ga:accountId">
                        <cfset entryStruct.accountId = entryNodes[num].property[i].XmlAttributes["value"] />
                    </cfcase>
                    <cfcase value="ga:accountName">
                        <cfset entryStruct.accountName = entryNodes[num].property[i].XmlAttributes["value"] />
                    </cfcase>
                    <cfcase value="ga:profileId">
                        <cfset entryStruct.profileId = entryNodes[num].property[i].XmlAttributes["value"] />
                    </cfcase>
                    <cfcase value="ga:webPropertyId">
                        <cfset entryStruct.webPropertyId = entryNodes[num].property[i].XmlAttributes["value"] />
                    </cfcase>
                </cfswitch>
            </cfloop>

            <cfset arrayAppend(profileArray,duplicate(entryStruct)) />

        </cfloop>

        <cfreturn profileArray />

    </cffunction>

A default date range of one year is set initially. A dialog box that allows the user to change the date range is presented as an option. The date range is limited to a max date of yesterday because GA does not have full data for the current day. This causes the averages that are calculated on the return data to be skewed.

A data feed request url is sent to GA along with the auth token a via cfhttp tag.

<cfset reqUrl = "https://www.google.com/analytics/feeds/data?ids=" & session.tableId & "&metrics=ga:newVisits,ga:pageviews,ga:visits,ga:visitors,ga:timeOnSite&start-date=" & session.startdate & "&end-date=" & session.enddate />

The XML response is then parsed out and returned as an array of structures.

<cffunction name="parseData" access="public" returntype="array" hint="GA data as array of structures">
        <cfargument name="gaUrl" type="string" required="yes">
        <cfargument name="authToken" type="string" required="yes">

        <cfset var returnArray = ArrayNew(1) />
        <cfset var entryStruct = StructNew() />

         <cfset entryNodes = callApi(arguments.gaUrl,arguments.authToken) />

         <!---loop through the entries and put each data point for each campaign in structure--->
            <cfloop from="1" to="#ArrayLen(entryNodes)#" index="num">

                <!---rest of the stats data from GA
                first check if dimension exists--->
                <cfif StructKeyExists(entryNodes[num],"dimension")>
                    <cfloop from="1" to="#ArrayLen(entryNodes[num].dimension)#" index="i">
                     <cfset "entryStruct.#Mid(entryNodes[num].dimension[i].XmlAttributes["name"],4,
Len(entryNodes[num].dimension[i].XmlAttributes["name"]))#" = entryNodes[num].dimension[i].XmlAttributes["value"] />
                     </cfloop>
                 </cfif>

                 <cfloop from="1" to="#ArrayLen(entryNodes[num].metric)#" index="i">
                         <cfset "entryStruct.#Mid(entryNodes[num].metric[i].XmlAttributes["name"],4,
Len(entryNodes[num].metric[i].XmlAttributes["name"]))#" = entryNodes[num].metric[i].XmlAttributes["value"] />
                  </cfloop>

                <cfset arrayAppend(returnArray,duplicate(entryStruct)) />

           </cfloop>

        <cfreturn returnArray />
    </cffunction>

Once the data is in a form you can manipulate, graphs, tables, and charts of all kinds can be made.

Recommended Reading

Demo

2 Comments for ColdFusion and Google Analytics: Getting Out What You Put In


Benedict Lowndes
February 19, 2010

Great work! Have you got any interest in packaging this up as a download, or even better, adding it to riaforge or github?


jen
February 20, 2010

@Benedict

I would like to present it at CFUnited this year if I get voted in. http://bit.ly/buna2K

If I don't get voted in, I'll post it. If I do, I'll post it after the conference.

Thanks!

Leave a comment

Why ask?

 

« | »