├── .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 | --------------------------------------------------------------------------------