jekyll-ga-v2 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ require "jekyll-ga-v2/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "jekyll-ga-v2"
9
+ spec.summary = "Jekyll Google Analytics integration"
10
+ spec.description = "Google Analytics support in Jekyll blog to easily show the statistics on your website"
11
+ spec.version = Jekyll::GoogleAnalyticsV2::VERSION
12
+ spec.authors = ["z3nth10n"]
13
+ spec.email = ["z3nth10n@gmail.com"]
14
+ spec.homepage = "https://github.com/uta-org/jekyll-ga-v2"
15
+ spec.licenses = ["MIT"]
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r!^(test|spec|features|assets|versions)/!) }
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "jekyll", "~> 3.0"
21
+ spec.add_dependency 'googleauth', '~> 0.8.0'
22
+ spec.add_dependency 'google-api-client', '~> 0.28.4'
23
+ spec.add_dependency 'chronic', '~> 0.10.2'
24
+
25
+ spec.add_development_dependency "rake", "~> 11.0"
26
+ spec.add_development_dependency "rspec", "~> 3.5"
27
+ end
@@ -0,0 +1,15 @@
1
+ # Jekyll::GoogleAnalyticsV2 is a gem built for Jekyll 3 that generates statics on your Blog
2
+ #
3
+ # Author: z3nth10n (United Teamwork Association)
4
+ # Site: https://github.com/uta-org/jekyll-patreon
5
+ # Distributed Under The MIT License (MIT) as described in the LICENSE file
6
+ # - https://opensource.org/licenses/MIT
7
+
8
+ require "jekyll-ga-v2/version"
9
+ # Files needed in general
10
+ require "jekyll-ga-v2/jekyll-ga-v2"
11
+
12
+ module Jekyll
13
+ module GoogleAnalyticsV2
14
+ end # module GoogleAnalyticsV2
15
+ end # module Jekyll
@@ -0,0 +1,292 @@
1
+ # Adapted from https://code.google.com/p/google-api-ruby-analytics/
2
+
3
+ require 'jekyll'
4
+ require 'rubygems'
5
+ require 'googleauth'
6
+ require 'google/apis/analytics_v3'
7
+ require 'chronic'
8
+ require 'json'
9
+
10
+ module Jekyll
11
+
12
+ class GoogleAnalytics < Generator
13
+ priority :highest
14
+
15
+ @response_data = nil
16
+ @past_response = nil
17
+ @headers = nil
18
+
19
+ def generate(site)
20
+ unless site.config['jekyll_ga']
21
+ return
22
+ end
23
+
24
+ Jekyll.logger.info "Jekyll GA:","Initializating"
25
+ startTime = Time.now
26
+
27
+ # Set "ga" to store the current config
28
+ ga = site.config['jekyll_ga']
29
+
30
+ # Local cache setup so we don't hit the sever X amount of times for same data.
31
+ cache_directory = ga['cache_directory'] || "_jekyll_ga"
32
+ cache_filename = ga['cache_filename'] || "ga_cache.json"
33
+ cache_file_path = cache_directory + "/" + cache_filename
34
+
35
+ # Set the refresh rate in minutes (how long the program will wait in minutes before writing a new file)
36
+ refresh_rate = ga['refresh_rate'] || 60
37
+
38
+ # If the directory doesn't exist lets make it
39
+ if not Dir.exist?(cache_directory)
40
+ Dir.mkdir(cache_directory)
41
+ end
42
+
43
+ # Now lets check for the cache file and how old it is (if it exceeds refesh_rate then update it)
44
+ if File.exist?(cache_file_path) and ((Time.now - File.mtime(cache_file_path)) / 60 < refresh_rate) and !ga["debug"]
45
+ # Inject from cache
46
+ data = JSON.parse(File.read(cache_file_path))
47
+
48
+ # Into pages...
49
+ site.pages.each { |page|
50
+ page.data["stats"] = data["page-stats"][get_identifier_for("page", page)]
51
+ }
52
+
53
+ # Into posts...
54
+ site.posts.docs.each { |post|
55
+ post.data["stats"] = data["post-stats"][get_identifier_for("post", post)]
56
+ }
57
+
58
+ # Into site...
59
+ site.data["stats"] = data["site-stats"]
60
+ site.data["period"] = data["period"]
61
+ site.data["headers"] = data["headers"]
62
+ else
63
+ analytics = Google::Apis::AnalyticsV3::AnalyticsService.new
64
+
65
+ # Load our credentials for the service account (using env vars)
66
+ auth = ::Google::Auth::ServiceAccountCredentials
67
+ .make_creds(scope: 'https://www.googleapis.com/auth/analytics')
68
+
69
+ # Assign auth
70
+ analytics.authorization = auth
71
+
72
+ # Get pages && posts from site (filtering its urls)
73
+ pages = site.pages.select { |page| page.name.include? ".html" }.collect { |page| filter_url(page.dir + page.name) }
74
+ posts = site.posts.docs.collect { |doc| filter_url(doc.url.to_s) }
75
+
76
+ # Concat the two arrays
77
+ pages.push(*posts)
78
+
79
+ # Create a queryString (string type) from the array
80
+ queryString = pages.collect { |page| "ga:pagePath==#{page.to_s}" }.join(",")
81
+
82
+ # Get the response
83
+ response = get_response(analytics, ga, queryString)
84
+
85
+ # Declare the hash where the info will go
86
+ store_data = {}
87
+
88
+ # Make another request to Google Analytics API to get the pasterence
89
+ if ga["compare_period"]
90
+ start_date = Chronic.parse(ga['start']).strftime("%Y-%m-%d")
91
+ end_date = Chronic.parse(ga['end']).strftime("%Y-%m-%d")
92
+
93
+ diff_date = end_date.to_date - start_date.to_date
94
+ diff_date = diff_date.numerator.to_i - 1
95
+
96
+ site.data["period"] = diff_date
97
+ store_data.store("period", diff_date)
98
+
99
+ @past_response = get_response(analytics, ga, queryString, start_date.to_date - diff_date, start_date)
100
+ end
101
+
102
+ # If there are errors then show them
103
+ if response.kind_of?(Array) and response.include? "error"
104
+ errors = reponse["error"]["errors"]
105
+
106
+ errors.each { |error|
107
+ Jekyll.logger.error "Jekyll GoogleAnalytics:", "Client Execute Error: #{error.message}"
108
+ }
109
+
110
+ raise RuntimeError, "Check errors from Google Analytics"
111
+ end
112
+
113
+ @response_data = response
114
+
115
+ # Get keys from columnHeaders
116
+ @headers = @response_data.column_headers.collect { |header| header.name.sub("ga:", "") }
117
+
118
+ # Loop through pages && posts to add the stats object value
119
+ page_data = {}
120
+ site.pages.each { |page|
121
+ stats_data = get_stats_for(ga, "page", page)
122
+ page.data["stats"] = stats_data
123
+
124
+ # Jekyll.logger.info "GA-debug (stats-type): ", page.data["statistics"].class.to_s
125
+
126
+ unless stats_data.nil?
127
+ page_data.store(get_identifier_for("page", page), stats_data)
128
+
129
+ if ga["debug"]
130
+ Jekyll.logger.info "GA-debug (page-stats): ", page.data["stats"].to_json
131
+ end
132
+ end
133
+ }
134
+ store_data.store("page-stats", page_data)
135
+
136
+ post_data = {}
137
+ site.posts.docs.each { |post|
138
+ stats_data = get_stats_for(ga, "post", post)
139
+ post.data["stats"] = stats_data
140
+
141
+ unless stats_data.nil?
142
+ post_data.store(get_identifier_for("post", post), stats_data)
143
+
144
+ if ga["debug"]
145
+ Jekyll.logger.info "GA-debug (post-stats): ", post.data["stats"].to_json
146
+ end
147
+ end
148
+ }
149
+ store_data.store("post-stats", post_data)
150
+
151
+ # Do the same for the site
152
+ stats_data = get_stats_for(ga, "site")
153
+ site.data["stats"] = stats_data
154
+
155
+ store_data.store("site-stats", stats_data)
156
+
157
+ if !stats_data.nil? and ga["debug"]
158
+ Jekyll.logger.info "GA-debug (site-stats): ", site.data["stats"].to_json
159
+ end
160
+
161
+ # Before saving modify headers
162
+
163
+ # Create a new array with the value, the diff and the perc from the last stored stats_data corresponding to the site one
164
+ new_headers = []
165
+ @headers.each { |header|
166
+ unless stats_data[header].nil?
167
+ protoheader = {}
168
+
169
+ protoheader.store("name", header)
170
+ protoheader.store("value", stats_data[header])
171
+ protoheader.store("diff_value", stats_data["diff_#{header}"])
172
+ protoheader.store("value_perc", stats_data["#{header}_perc"])
173
+
174
+ new_headers.push(protoheader)
175
+ end
176
+ }
177
+
178
+ # Then save...
179
+ site.data["headers"] = new_headers
180
+ store_data.store("headers", new_headers)
181
+
182
+ # Write the response data
183
+ if File.exist?(cache_file_path) and ((Time.now - File.mtime(cache_file_path)) / 60 >= refresh_rate) and ga["debug"] or !ga["debug"] or !File.exist?(cache_file_path)
184
+ File.open(cache_file_path, "w") do |f|
185
+ f.write(JSON.pretty_generate(store_data))
186
+ end
187
+ end
188
+ end
189
+
190
+ endTime = Time.now - startTime
191
+
192
+ Jekyll.logger.info "Jekyll GoogleAnalytics:", "Initializated in #{endTime} seconds"
193
+ end
194
+
195
+ def get_identifier_for(page_type, inst)
196
+ if page_type == "page"
197
+ return filter_url(inst.dir + inst.name)
198
+ elsif page_type == "post"
199
+ return filter_url(inst.url.to_s)
200
+ end
201
+ end
202
+
203
+ def get_stats_for(ga, page_type, inst = nil)
204
+ data = nil
205
+ past_data = nil
206
+
207
+ # Transpose array into hash using columnHeaders
208
+ if page_type == "page"
209
+ data = @response_data.rows.select { |row| row[0] == filter_url(inst.dir + inst.name) }.collect { |row| Hash[ [@headers, row].transpose ] }[0]
210
+ past_data = @past_response.nil? or !@past_response.nil? and @past_response.rows.nil? ? nil : @past_response.rows.select { |row| row[0] == filter_url(inst.dir + inst.name) }.collect { |row| Hash[ [@headers, row].transpose ] }[0]
211
+ elsif page_type == "post"
212
+ data = @response_data.rows.select { |row| row[0] == filter_url(inst.url.to_s) }.collect { |row| Hash[ [@headers, row].transpose ] }[0]
213
+ past_data = @past_response.nil? or !@past_response.nil? and @past_response.rows.nil? ? nil : @past_response.rows.select { |row| row[0] == filter_url(inst.url.to_s) }.collect { |row| Hash[ [@headers, row].transpose ] }[0]
214
+ elsif page_type == "site"
215
+ data = get_site_data(false)
216
+ past_data = get_site_data(true)
217
+ end
218
+
219
+ if data.nil?
220
+ return nil
221
+ end
222
+
223
+ # Create diff_xxx and xxx_perc keys for data
224
+ if ga["compare_period"]
225
+ pre_data = {}
226
+
227
+ data.each { |key, value|
228
+ present_value = value.to_f
229
+
230
+ past_value = nil
231
+
232
+ if past_data.kind_of?(Hash)
233
+ past_value = past_data.fetch(key, 0.0).to_f
234
+ else
235
+ past_value = 0.0
236
+ end
237
+
238
+ if float?(value) and float?(past_value) # Filter for pagePath (not float or integer value)
239
+ # Thanks to: https://stackoverflow.com/q/31981133/3286975
240
+ diff_value = present_value - past_value
241
+ perc_value = present_value / past_value * 100.0
242
+
243
+ pre_data.store("diff_#{key}", diff_value)
244
+ pre_data.store("#{key}_perc", perc_value == Float::INFINITY ? "∞" : (perc_value.nan? ? "0" : perc_value.to_s))
245
+ end
246
+ }
247
+
248
+ data.merge!(pre_data)
249
+ end
250
+
251
+ return data
252
+ end
253
+
254
+ def get_site_data(is_past)
255
+ data = (is_past ? @past_response : @response_data).totals_for_all_results
256
+ data.keys.each { |k| data[k.sub("ga:", "")] = data[k]; data.delete(k) }
257
+
258
+ return data;
259
+ end
260
+
261
+ def get_response(analytics, ga, queryString, tstart = nil, tend = nil)
262
+ return analytics.get_ga_data(
263
+ ga['profileID'], # ids
264
+ tstart.nil? ? Chronic.parse(ga['start']).strftime("%Y-%m-%d") : tstart.to_s, # start_date
265
+ tend.nil? ? Chronic.parse(ga['end']).strftime("%Y-%m-%d") : tend.to_s, # end_date
266
+ ga['metrics'], # metrics
267
+ dimensions: ga['dimensions'],
268
+ filters: ga["filters"].to_s.empty? ? queryString : ga["filters"].to_s,
269
+ include_empty_rows: nil,
270
+ max_results: ga["max_results"].nil? ? 10000 : ga["max_results"].to_i,
271
+ output: nil,
272
+ sampling_level: nil,
273
+ segment: ga['segment'])
274
+ end
275
+
276
+ def filter_url(url)
277
+ if url.include? ".html"
278
+ url = url.sub(".html", "")
279
+ end
280
+
281
+ if url.include? "index"
282
+ url = url.sub("index", "")
283
+ end
284
+
285
+ return url
286
+ end
287
+
288
+ def float?(string)
289
+ true if Float(string) rescue false
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,5 @@
1
+ module Jekyll
2
+ module GoogleAnalyticsV2
3
+ VERSION = "1.0.0"
4
+ end # module GoogleAnalyticsV2
5
+ end # module Jekyll
data/readme.md ADDED
@@ -0,0 +1,204 @@
1
+ # jekyll-ga-v2 [![Build Status](https://travis-ci.org/uta-org/jekyll-ga-v2.svg?branch=master)](https://travis-ci.org/uta-org/jekyll-ga-v2) [![Gem Version](https://badge.fury.io/rb/jekyll-ga-v2.svg)](http://badge.fury.io/rb/jekyll-ga-v2)
2
+
3
+ Requires Ruby 2.5+ and Jekyll 3.8+
4
+
5
+ > A Jekyll plugin that downloads Google Analytics data and adds it to your Jekyll website. The Google Analytics metric is added to each post/page's metadata and is accessible as `page.stats`. It can be printed in a template.
6
+
7
+ ## Installation
8
+
9
+ This plugin requires three Ruby gems:
10
+
11
+ ```bash
12
+ $ sudo gem install chronic
13
+ $ sudo gem install google-api-client
14
+ $ sudo gem install googleauth
15
+ ```
16
+
17
+ Add this line to your site's Gemfile:
18
+
19
+ ```ruby
20
+ gem 'jekyll-ga-v2'
21
+ ```
22
+
23
+ ### Set up a service account for the Google data API
24
+
25
+ - Go to https://code.google.com/apis/console/b/0/ and create a new project.
26
+ - Turn on the Analytics API and accept the terms of service
27
+ - Go to `API Access` on the left sidebar menu, create a new oauth 2.0 client ID, give your project a name, and click `next`.
28
+ - Select Application type: `Service account`, and click `Create client ID`
29
+ - note the private key's password. It will probably be `notasecret` unless Google changes something. You'll need to enter this value in your configuration settings.
30
+ - Download the private key. Save this file because you can only download it once.
31
+ - Note the `Email address` for the Service account. You'll need this for your configuration settings and in the next step.
32
+ - Log into Google Analytics and add the service account email address as a user of your Google Analytics profile: From a report page, `Admin > select a profile > Users > New User`
33
+
34
+ #### Configuration of the environment variables
35
+
36
+ [GoogleAuth needs the following environment variables to work.](https://github.com/googleapis/google-auth-library-ruby#example-environment-variables)
37
+
38
+ There is an easy way to implement this using CircleCI (maybe you are using similar to deploy your Jekyll website). If you're not familiar with CircleCI you'll need to read carefully this post on my blog about "[How To Use Any Jekyll Plugins on GitHub Pages with CircleCI](https://z3nth10n.github.io/en/2019/03/20/jekyll-plugin-issue-with-github-pages)".
39
+
40
+ Once you implement it, you'll need to go to your [CircleCI dashboard](https://circleci.com/dashboard) search your project settings and go under "**Organization > Contexts**" and create [a new Context](https://circleci.com/docs/2.0/contexts/).
41
+
42
+ Look at my website [CircleCI.yml configuration here](https://github.com/z3nth10n/z3nth10n.github.io/blob/b9f7ef42e5fce33800aab80f8eabe6868b38f8e5/circle.yml#L54). The only thing remaining is to create the appropiate Context name, and then, create the required env vars:
43
+
44
+ ![](https://i.gyazo.com/3ad97b8e09ee7e05b8496f1cd631affa.png)
45
+
46
+ **Note:** The `GOOGLE_PRIVATE_KEY` value is the output from OpenSSL. You'll need to execute the following command to get it from the `*.p12` file:
47
+
48
+ ```bash
49
+ $ openssl pkcs12 -in filename.p12 -clcerts -nodes -nocerts
50
+ ```
51
+
52
+ You'll need to replace all the new lines characters by `\n`. This can be easily done with Sublime Text 3 specifying the Regex options and the replacing `\n` by `\\n`.
53
+
54
+ ## Configuration
55
+
56
+ To configure `jekyll-ga-v2`, you need to specify some information about your Google Analytics service account (as set up above) and your report settings.
57
+
58
+ Add the following block to your Jekyll site's `_config.yml` file:
59
+
60
+ ```yml
61
+ ####################
62
+ # Google Analytics #
63
+ ####################
64
+
65
+ jekyll_ga:
66
+ profileID: ga:<user_id> # Profile ID
67
+ start: last week # Beginning of report
68
+ end: now # End of report
69
+ compare_period: true
70
+ metrics: ga:pageviews # Metrics code
71
+ dimensions: ga:pagePath # Dimensions
72
+ segment: # Optional
73
+ filters: # Optional
74
+ sort: true # Sort posts by this metric
75
+ max_results: 10000 # Number of the maximum results get by the API
76
+ debug: false # Debug mode
77
+ ```
78
+
79
+ * `profileID` is the specific report profile from which you want to pull data. Find it by going to the report page in Google Analytics. Look at the URL. It will look something like `https://www.google.com/analytics/web/?hl=en&pli=1#report/visitors-overview/###########p######/`. The number after the `p` at the end of the URL is your `profileID`.
80
+ * The `start` and `end` indicate the time range of data you want to query. They are parsed using Ruby's `Chronic` gem, so you can include relative or absolute dates, such as `now`, `yesterday`, `last month`, `2 weeks ago`. See [Chronic's documentation](https://github.com/mojombo/chronic#examples) for more options.
81
+ * The `metrics` value is what you want to measure from your Google Analytics data. Usually this will be `ga:pageviews` or `ga:visits`, but it can be any metric available in Google Analytics. Specify only one. See the [Google Analytics Query Explorer](http://ga-dev-tools.appspot.com/explorer/?csw=1) to experiment with different metrics. (Your `dimension` should always be `ga:pagePath`). I recommend you the following string `ga:pageviews,ga:bounceRate,ga:sessions,ga:users,ga:newUsers`.
82
+ * The `segment` and `filters` keys are optional parameters for your query. See the [Google Analytics Query Explorer](http://ga-dev-tools.appspot.com/explorer/?csw=1) for a description of how to use them, or just leave them out.
83
+ * The `sort` key can be `true` or `false`. If `true`, your posts will be sorted first by your Google Analytics metic, then chronologically as is the default. If `false` or not specified, your posts will sort as usual.
84
+
85
+ New params in v2:
86
+
87
+ * If `compare_period` is to true, then this will create two reports (**example:** if start is set to "last month", this will create one report from "end" to "start" and the second report its end will be at the start of the first report, with this data a comparation will be created).
88
+
89
+ ### Need help for examples?
90
+
91
+ Look at those two HTML files I created to render my settings:
92
+
93
+ ```html
94
+ <div id="genstats" class="col-md-3 align-sm-right vertical-margin order-xs-fourth col-xs-expand">
95
+ <box class="both-offset expand-width">
96
+ <p>
97
+ <h3>Statistics</h3>
98
+ <p>(last {{ site.data.period }} days)</p>
99
+ </p>
100
+
101
+ {% for header in site.data.headers %}
102
+
103
+ <p>
104
+ {% assign hvalue = header.value | plus: 0 %}
105
+ {{ hvalue | round }} {{ header.name }}
106
+ </p>
107
+ <p class="sub">
108
+ {% if site.jekyll_ga.compare_period %}
109
+ (
110
+ last {{ site.data.period }} days:
111
+ {% if header.value_perc != "∞" %}
112
+ {% assign perc = header.value_perc | plus: 0 %}
113
+
114
+ {% if perc > 0 %}
115
+ <i class="fas fa-arrow-up color-green"></i>
116
+ {% elsif perc == 0 %}
117
+ <i class="fas fa-equals"></i>
118
+ {% elsif perc < 0 %}
119
+ <i class="fas fa-arrow-down color-red"></i>
120
+ {% endif %}
121
+
122
+ {{ perc | round }} % |
123
+
124
+ {% assign diff = header.diff_value %}
125
+ {% if diff > 0 %}+{% endif %}
126
+ {{ diff | round }} than last period
127
+ {% else %}
128
+ ∞ %
129
+ {% endif %}
130
+ )
131
+ {% endif %}
132
+ </p>
133
+
134
+ {% endfor %}
135
+ </box>
136
+ </div>
137
+ ```
138
+
139
+ This displays a box with the different metrics selected in your `metrics` configuration parameter:
140
+
141
+ ![](https://i.gyazo.com/3105ff73fc023c5cf3506b9adcd63577.png)
142
+
143
+ I use this for any post:
144
+
145
+ ```html
146
+ {% if page.stats.pageviews != blank %}
147
+ {% assign hvalue = header.value | plus: 0 %}
148
+ {{ hvalue | round }} views
149
+
150
+ {% if site.jekyll_ga.compare_period %}
151
+ (
152
+ last {{ site.data.period }} days:
153
+ {% if page.stats.pageviews_perc != "∞" %}
154
+ {% assign perc = page.stats.pageviews_perc | plus: 0 %}
155
+
156
+ {% if perc > 0 %}
157
+ <i class="fas fa-arrow-up color-green"></i>
158
+ {% elsif perc == 0 %}
159
+ <i class="fas fa-equals"></i>
160
+ {% elsif perc < 0 %}
161
+ <i class="fas fa-arrow-down color-red"></i>
162
+ {% endif %}
163
+
164
+ {{ perc | round }} % |
165
+
166
+ {% assign diff = page.stats.diff_pageviews %}
167
+ {% if diff > 0 %}+{% endif %}
168
+ {{ diff | round }} than last period
169
+ {% else %}
170
+ ∞ %
171
+ {% endif %}
172
+ )
173
+ {% endif %}
174
+ .
175
+ {% endif %}
176
+ ```
177
+
178
+ It only displays `xx visits (percentage % | difference between two ranges)`.
179
+
180
+ ## Issues
181
+
182
+ Having issues? Just report in [the issue section](/issues). **Thanks for the feedback!**
183
+
184
+ ## Contribute
185
+
186
+ Fork this repository, make your changes and then issue a pull request. If you find bugs or have new ideas that you do not want to implement yourself, file a bug report.
187
+
188
+ ## Donate
189
+
190
+ Become a patron, by simply clicking on this button (**very appreciated!**):
191
+
192
+ [![](https://c5.patreon.com/external/logo/become_a_patron_button.png)](https://www.patreon.com/z3nth10n)
193
+
194
+ ... Or if you prefer an one-time donation:
195
+
196
+ [![](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://paypal.me/z3nth10n)
197
+
198
+ ## Copyright
199
+
200
+ Copyright (c) 2019 z3nth10n (United Teamwork Association).
201
+
202
+ License: MIT
203
+
204
+ **Best regards.**