├── .gitignore
├── requirements.txt
├── app_config.py
├── chart_examples
├── index.html
└── dashboard_charts.js
├── reports.py
├── CONTRIBUTING.md
├── LICENSE.md
├── analytics_proxy.py
├── README.md
└── util_functions.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.p12
2 | *.dat
3 | *.sqlite
4 | *.pyc
5 | *.DS_Store
6 | *.jsons
7 | celerybeat-schedule.db
8 | .env
9 | *.rdb
10 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | Flask==0.10.1
2 | Jinja2==2.7.3
3 | MarkupSafe==0.23
4 | Werkzeug==0.10.4
5 | cffi==0.9.2
6 | cryptography==0.8.1
7 | enum34==1.0.4
8 | google-api-python-client==1.4.0
9 | httplib2==0.9
10 | itsdangerous==0.24
11 | oauth2client==1.4.7
12 | pyOpenSSL==0.14
13 | pyasn1==0.1.7
14 | pyasn1-modules==0.0.5
15 | pycparser==2.10
16 | redis==2.10.3
17 | rsa==3.1.4
18 | simplejson==3.6.5
19 | six==1.9.0
20 | uritemplate==0.6
21 | wsgiref==0.1.2
22 |
--------------------------------------------------------------------------------
/app_config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 |
4 | class Config(object):
5 | DEBUG = True
6 | TESTING = True
7 | CSRF_ENABLED = True
8 | SECRET_KEY = os.getenv('SECRET_KEY') or 'Default Key'
9 | CLIENT_EMAIL = os.getenv('CLIENT_EMAIL')
10 | GA_P12_KEY = os.getenv('GA_P12_KEY')
11 | REDIS_HOST = os.getenv('REDIS_HOST') or 'localhost'
12 |
13 |
14 | class ProductionConfig(Config):
15 | DEBUG = False
16 |
17 |
18 | class StagingConfig(Config):
19 | DEVELOPMENT = True
20 | DEBUG = True
21 |
22 |
23 | class DevelopmentConfig(Config):
24 | DEVELOPMENT = True
25 | DEBUG = True
26 |
27 |
28 | class TestingConfig(Config):
29 | TESTING = True
30 |
--------------------------------------------------------------------------------
/chart_examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | test
4 |
7 |
8 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/reports.py:
--------------------------------------------------------------------------------
1 | """
2 | This is an examples reports_dict.
3 | report_name: Name of report and url path of the report
4 | refresh_rate: How long report data will remain in cache
5 | query: A Google Analyics query.
6 | """
7 | report_dict = [
8 | {
9 | 'report_name': 'top-sources',
10 | 'refresh_rate': 60,
11 | 'query': {
12 | 'ids': 'ga:86930627',
13 | 'dimensions': 'ga:source',
14 | 'metrics': 'ga:sessions',
15 | 'start_date': '2013-11-20',
16 | 'end_date': '2015-11-30'
17 | }
18 | },
19 | {
20 | 'report_name': 'top-pages',
21 | 'refresh_rate': 60,
22 | 'query': {
23 | 'ids': 'ga:86930627',
24 | 'dimensions': 'ga:pageTitle',
25 | 'metrics': 'ga:sessions',
26 | 'start_date': '2013-11-20',
27 | 'end_date': '2015-11-30'
28 | }
29 | },
30 | {
31 | 'report_name': 'top-countries',
32 | 'refresh_rate': 60,
33 | 'query': {
34 | 'ids': 'ga:86930627',
35 | 'dimensions': 'ga:country',
36 | 'metrics': 'ga:sessions',
37 | 'start_date': '2013-11-20',
38 | 'end_date': '2015-11-30'
39 | }
40 | }
41 |
42 | ]
43 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Welcome!
2 |
3 | We're so glad you're thinking about contributing to an 18F open source project! If you're unsure about anything, just ask -- or submit the issue or pull request anyway. The worst that can happen is you'll be politely asked to change something. We love all friendly contributions.
4 |
5 | We want to ensure a welcoming environment for all of our projects. Our staff follow the [18F Code of Conduct](https://github.com/18F/code-of-conduct/blob/master/code-of-conduct.md) and all contributors should do the same.
6 |
7 | We encourage you to read this project's CONTRIBUTING policy (you are here), its [LICENSE](LICENSE.md), and its [README](README.md).
8 |
9 | If you have any questions or want to read more, check out the [18F Open Source Policy GitHub repository]( https://github.com/18f/open-source-policy), or just [shoot us an email](mailto:18f@gsa.gov).
10 |
11 | ## Public domain
12 |
13 | This project is in the public domain within the United States, and
14 | copyright and related rights in the work worldwide are waived through
15 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/).
16 |
17 | All contributions to this project will be released under the CC0
18 | dedication. By submitting a pull request, you are agreeing to comply
19 | with this waiver of copyright interest.
20 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | As a work of the United States Government, this project is in the
2 | public domain within the United States.
3 |
4 | Additionally, we waive copyright and related rights in the work
5 | worldwide through the CC0 1.0 Universal public domain dedication.
6 |
7 | ## CC0 1.0 Universal Summary
8 |
9 | This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode).
10 |
11 | ### No Copyright
12 |
13 | The person who associated a work with this deed has dedicated the work to
14 | the public domain by waiving all of his or her rights to the work worldwide
15 | under copyright law, including all related and neighboring rights, to the
16 | extent allowed by law.
17 |
18 | You can copy, modify, distribute and perform the work, even for commercial
19 | purposes, all without asking permission.
20 |
21 | ### Other Information
22 |
23 | In no way are the patent or trademark rights of any person affected by CC0,
24 | nor are the rights that other persons may have in the work or in how the
25 | work is used, such as publicity or privacy rights.
26 |
27 | Unless expressly stated otherwise, the person who associated a work with
28 | this deed makes no warranties about the work, and disclaims liability for
29 | all uses of the work, to the fullest extent permitted by applicable law.
30 | When using or citing the work, you should not imply endorsement by the
31 | author or the affirmer.
32 |
--------------------------------------------------------------------------------
/chart_examples/dashboard_charts.js:
--------------------------------------------------------------------------------
1 | google.setOnLoadCallback(drawVisualization);
2 |
3 | function httpGet_to_array(theUrl)
4 | {
5 | var xmlHttp = null;
6 |
7 | xmlHttp = new XMLHttpRequest();
8 | xmlHttp.open( "GET", theUrl, false );
9 | xmlHttp.send( null );
10 | var array = JSON.parse( xmlHttp.responseText );
11 | var data = [];
12 | cols = Object.keys(array['data'][0]);
13 | data.push(cols)
14 | for(var i = 0; i < array['data'].length; i++ ){
15 | var row = [];
16 | for(var j = 0; k=j < cols.length; j++){
17 | row.push(array['data'][i][cols[j]]);
18 | }
19 | data.push(row)
20 | }
21 | return data;
22 | }
23 |
24 | function drawVisualization() {
25 |
26 | var data = new google.visualization.arrayToDataTable(httpGet_to_array("http://127.0.0.1:5000/data/top-countries"));
27 |
28 | var regionTable = new google.visualization.ChartWrapper({
29 | "containerId": 'region_table',
30 | "chartType": 'Table',
31 | "refreshInterval": 1000,
32 | "dataTable": data,
33 | "options": {
34 | "showRowNumber" : true,
35 | "width": 630,
36 | "height": 440,
37 | "is3D": false,
38 | "title": "Sessions by Day"
39 | }
40 | });
41 |
42 | regionTable.draw();
43 |
44 | var options = {
45 | "width": 630,
46 | "height": 440,
47 | "title": "Visitors by Region"
48 | };
49 |
50 | var regionChart = new google.visualization.GeoChart(document.getElementById('regions_map'));
51 |
52 | regionChart.draw(data, options);
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/analytics_proxy.py:
--------------------------------------------------------------------------------
1 | import os
2 | import redis
3 | import pickle
4 |
5 | from flask import Flask, Response, jsonify
6 |
7 | from util_functions import (
8 | initialize_service, call_api, prepare_data, load_reports, crossdomain)
9 |
10 | app = Flask(__name__)
11 | app.config.from_object(os.getenv('APP_SETTINGS'))
12 | app.redis = redis.StrictRedis(host=app.config['REDIS_HOST'], port=6379, db=0)
13 | load_reports(app.redis)
14 |
15 |
16 | @app.route("/", methods=['GET'])
17 | def index():
18 | return "Project Info"
19 |
20 |
21 | @app.route("/data/", methods=['GET'])
22 | @crossdomain(origin='*')
23 | def get_analytics(report_name):
24 |
25 | report = app.redis.get(report_name)
26 | if not report:
27 | response = Response(
28 | "{'error': 'No Report'}",
29 | status=200,
30 | mimetype='application/json'
31 | )
32 | else:
33 | report = pickle.loads(report)
34 | redis_data = app.redis.get(report_name + '_data')
35 | if not redis_data:
36 | ga_api_service = initialize_service(app.config)
37 | data = prepare_data(
38 | call_api(query=report['query'], service=ga_api_service))
39 | app.redis.set(report_name + '_data', pickle.dumps(data))
40 | app.redis.expire(report_name + '_data', report['refresh_rate'])
41 | else:
42 | data = pickle.loads(redis_data)
43 |
44 | data['report'] = report
45 | response = jsonify(data)
46 |
47 | return response
48 |
49 |
50 | if __name__ == "__main__":
51 | port = int(os.getenv('PORT', 5000))
52 | app.run(host='0.0.0.0', port=port)
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Analytics Proxy
2 |
3 | Analytics Proxy allows you to publicly share Google Analytics reporting data. It was partially inspired by [Google Analytics superProxy](https://github.com/googleanalytics/google-analytics-super-proxy); however, unlike [Google Analytics superProxy](https://github.com/googleanalytics/google-analytics-super-proxy) it doesn’t need to be deployed on [Google App Engine](https://appengine.google.com/)
4 |
5 | ## Setup
6 |
7 | ### Requirements
8 | - [Python 2.7](https://docs.python.org/2/)
9 | - [Redis](http://redis.io/)
10 |
11 | ### Google Analytics Service Account
12 | 1. [Create Google API service account and take not of the client email](https://developers.google.com/accounts/docs/OAuth2ServiceAccount).
13 | 2. Download the P12 private key file and place it in the analytics-proxy folder.
14 | 3. Make sure to add the client email to the Google Analytics account.
15 |
16 | ### Install Redis
17 | ```bash
18 | # Mac OS
19 | brew install redis
20 | # Ubuntu
21 | sudo apt-get install redis-server
22 | ```
23 |
24 | ### Start Redis
25 | ```
26 | redis-server
27 | ```
28 |
29 | ### Env Variables
30 | ```
31 | export APP_SETTINGS="app_config.DevelopmentConfig"
32 | export CLIENT_EMAIL=<>
33 | export GA_P12_KEY=<>
34 | ```
35 |
36 | ### Modify `reports.py` to meet reporting needs.
37 | ```
38 | {
39 | 'report_name': 'top-sources',
40 | 'refresh_rate': 60,
41 | 'query': {
42 | 'ids': 'ga:<>',
43 | 'dimensions': 'ga:source',
44 | 'metrics': 'ga:sessions',
45 | 'start_date': '2013-11-20',
46 | 'end_date': '2015-11-30'
47 | }
48 | },
49 | ```
50 |
--------------------------------------------------------------------------------
/util_functions.py:
--------------------------------------------------------------------------------
1 | import pickle
2 |
3 | from oauth2client.client import SignedJwtAssertionCredentials
4 | from httplib2 import Http
5 | from apiclient.discovery import build
6 | from flask import make_response, request, current_app
7 | from datetime import timedelta
8 | from functools import update_wrapper
9 |
10 |
11 | def initialize_service(config):
12 | """ Initalizes google analytics service """
13 |
14 | client_email = config['CLIENT_EMAIL']
15 | with open(config['GA_P12_KEY'], 'r') as f:
16 | private_key = f.read()
17 | credentials = SignedJwtAssertionCredentials(
18 | client_email, private_key,
19 | 'https://www.googleapis.com/auth/analytics.readonly')
20 | http_auth = credentials.authorize(Http())
21 |
22 | return build('analytics', 'v3', http=http_auth)
23 |
24 |
25 | def call_api(query, service):
26 | """ calls api and returns result """
27 | result = service.data().ga().get(**query).execute()
28 | return result
29 |
30 |
31 | def prepare_data(result):
32 | """ Prepares data to return """
33 | header = [col['name'].strip("ga:") for col in result['columnHeaders']]
34 | data = []
35 | for row in result.get('rows'):
36 | data.append(dict(zip(header, row)))
37 | return {'data': data}
38 |
39 |
40 | def load_reports(redis_client):
41 | """ Loads reports into redis """
42 | from reports import report_dict
43 | for item in report_dict:
44 | redis_client.set(item['report_name'], pickle.dumps(item))
45 |
46 |
47 | def crossdomain(origin=None, methods=None, headers=None,
48 | max_age=21600, attach_to_all=True,
49 | automatic_options=True):
50 | if methods is not None:
51 | methods = ', '.join(sorted(x.upper() for x in methods))
52 | if headers is not None and not isinstance(headers, basestring):
53 | headers = ', '.join(x.upper() for x in headers)
54 | if not isinstance(origin, basestring):
55 | origin = ', '.join(origin)
56 | if isinstance(max_age, timedelta):
57 | max_age = max_age.total_seconds()
58 |
59 | def get_methods():
60 | if methods is not None:
61 | return methods
62 |
63 | options_resp = current_app.make_default_options_response()
64 | return options_resp.headers['allow']
65 |
66 | def decorator(f):
67 | def wrapped_function(*args, **kwargs):
68 | if automatic_options and request.method == 'OPTIONS':
69 | resp = current_app.make_default_options_response()
70 | else:
71 | resp = make_response(f(*args, **kwargs))
72 | if not attach_to_all and request.method != 'OPTIONS':
73 | return resp
74 |
75 | h = resp.headers
76 |
77 | h['Access-Control-Allow-Origin'] = origin
78 | h['Access-Control-Allow-Methods'] = get_methods()
79 | h['Access-Control-Max-Age'] = str(max_age)
80 | if headers is not None:
81 | h['Access-Control-Allow-Headers'] = headers
82 | return resp
83 |
84 | f.provide_automatic_options = False
85 | return update_wrapper(wrapped_function, f)
86 | return decorator
87 |
--------------------------------------------------------------------------------