From f04b0188051aa9e4a29d06607f79909545affbde Mon Sep 17 00:00:00 2001 From: Jake Moshenko Date: Thu, 13 Oct 2016 13:48:35 -0400 Subject: [PATCH] Write our users to Marketo as leads. --- app.py | 2 + config.py | 2 +- endpoints/api/user.py | 10 ++- endpoints/common.py | 8 +- endpoints/web.py | 3 +- requirements-nover.txt | 1 + requirements.txt | 4 +- static/js/services/user-service.js | 20 +++++ templates/base.html | 6 ++ util/asyncwrapper.py | 63 +++++++++++++ util/saas/useranalytics.py | 137 +++++++++++++++++++++++++++++ 11 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 util/asyncwrapper.py create mode 100644 util/saas/useranalytics.py diff --git a/app.py b/app.py index 2ad2fe618..214efb3b6 100644 --- a/app.py +++ b/app.py @@ -26,6 +26,7 @@ from data.userevent import UserEventsBuilderModule from data.queue import WorkQueue, BuildMetricQueueReporter from util import get_app_url from util.saas.analytics import Analytics +from util.saas.useranalytics import UserAnalytics from util.saas.exceptionlog import Sentry from util.names import urn_generator from util.config.oauth import (GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig, @@ -177,6 +178,7 @@ storage = Storage(app, metric_queue, instance_keys) userfiles = Userfiles(app, storage) log_archive = LogArchive(app, storage) analytics = Analytics(app) +user_analytics = UserAnalytics(app) billing = Billing(app) sentry = Sentry(app) build_logs = BuildLogs(app) diff --git a/config.py b/config.py index f877afb7e..027f36254 100644 --- a/config.py +++ b/config.py @@ -19,7 +19,7 @@ CLIENT_WHITELIST = ['SERVER_HOSTNAME', 'PREFERRED_URL_SCHEME', 'MIXPANEL_KEY', 'STRIPE_PUBLISHABLE_KEY', 'ENTERPRISE_LOGO_URL', 'SENTRY_PUBLIC_DSN', 'AUTHENTICATION_TYPE', 'REGISTRY_TITLE', 'REGISTRY_TITLE_SHORT', 'CONTACT_INFO', 'AVATAR_KIND', 'LOCAL_OAUTH_HANDLER', 'DOCUMENTATION_LOCATION', - 'DOCUMENTATION_METADATA', 'SETUP_COMPLETE', 'DEBUG', 'MUNCHKIN_KEY'] + 'DOCUMENTATION_METADATA', 'SETUP_COMPLETE', 'DEBUG', 'MARKETO_MUNCHKIN_ID'] def frontend_visible_config(config_dict): diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 7547d014a..c6c75c0be 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -10,7 +10,7 @@ from peewee import IntegrityError import features -from app import app, billing as stripe, authentication, avatar +from app import app, billing as stripe, authentication, avatar, user_analytics from auth import scopes from auth.auth_context import get_authenticated_user from auth.permissions import (AdministerOrganizationPermission, CreateRepositoryPermission, @@ -119,6 +119,14 @@ def user_view(user): 'tag_expiration': user.removed_tag_expiration_s, }) + analytics_metadata = user_analytics.get_user_analytics_metadata(user) + + # This is a sync call, but goes through the async wrapper interface and + # returns a Future. By calling with timeout 0 immediately after the method + # call, we ensure that if it ever accidentally becomes async it will raise + # a TimeoutError. + user_response.update(analytics_metadata.result(timeout=0)) + user_view_perm = UserReadPermission(user.username) if user_view_perm.can(): user_response.update({ diff --git a/endpoints/common.py b/endpoints/common.py index 791f987f8..d00c4bc64 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -16,7 +16,7 @@ from flask_principal import identity_changed import endpoints.decorated # Register the various exceptions via decorators. import features -from app import app, oauth_apps, LoginWrappedDBUser +from app import app, oauth_apps, LoginWrappedDBUser, user_analytics from auth import scopes from auth.permissions import QuayDeferredPermissionUser from config import frontend_visible_config @@ -114,6 +114,10 @@ def common_login(db_user): new_identity = QuayDeferredPermissionUser.for_user(db_user) identity_changed.send(app, identity=new_identity) session['login_time'] = datetime.datetime.now() + + # Inform our user analytics that we have a new "lead" + user_analytics.create_lead(db_user.email, db_user.username) + return True else: logger.debug('User could not be logged in, inactive?.') @@ -209,7 +213,7 @@ def render_page_template(name, route_data=None, **kwargs): vuln_priority_set=json.dumps(PRIORITY_LEVELS), enterprise_logo=app.config.get('ENTERPRISE_LOGO_URL', ''), mixpanel_key=app.config.get('MIXPANEL_KEY', ''), - munchkin_key=app.config.get('MUNCHKIN_KEY', ''), + munchkin_key=app.config.get('MARKETO_MUNCHKIN_ID', ''), google_tagmanager_key=app.config.get('GOOGLE_TAGMANAGER_KEY', ''), google_anaytics_key=app.config.get('GOOGLE_ANALYTICS_KEY', ''), sentry_public_dsn=app.config.get('SENTRY_PUBLIC_DSN', ''), diff --git a/endpoints/web.py b/endpoints/web.py index 4b4d9f4e1..b421f9f86 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -11,7 +11,7 @@ from flask_login import current_user import features from app import (app, billing as stripe, build_logs, avatar, signer, log_archive, config_provider, - get_app_url, instance_keys) + get_app_url, instance_keys, user_analytics) from auth import scopes from auth.auth_context import get_authenticated_user from auth.permissions import (AdministerOrganizationPermission, ReadRepositoryPermission, @@ -390,6 +390,7 @@ def confirm_email(): if new_email: send_email_changed(user.username, old_email, new_email) + user_analytics.change_email(old_email, new_email) common_login(user) diff --git a/requirements-nover.txt b/requirements-nover.txt index f1c093b26..121492bc1 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -7,6 +7,7 @@ -e git+https://github.com/coreos/py-bitbucket.git#egg=py-bitbucket -e git+https://github.com/coreos/pyapi-gitlab.git@timeout#egg=pyapi-gitlab -e git+https://github.com/coreos/resumablehashlib.git#egg=resumablehashlib +-e git+https://github.com/jepcastelein/marketo-rest-python.git#egg=marketorestpython APScheduler==3.0.5 Flask-Login Flask-Mail diff --git a/requirements.txt b/requirements.txt index bbbeef29a..b2083ecf1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,6 +47,7 @@ jsonschema==2.5.1 keystoneauth1==2.4.0 Mako==1.0.4 marisa-trie==0.7.2 +-e git+https://github.com/jepcastelein/marketo-rest-python.git@1ba6dfee030b192f0930dd8c3b6d53b52d886c65#egg=marketorestpython-master MarkupSafe==0.23 mixpanel==4.3.0 mock==2.0.0 @@ -98,7 +99,7 @@ raven==5.12.0 redis==2.10.5 redlock==1.2.0 reportlab==2.7 -requests==2.9.1 +requests==2.11.1 requests-oauthlib==0.6.1 -e git+https://github.com/coreos/resumablehashlib.git@b1b631249589b07adf40e0ee545b323a501340b4#egg=resumablehashlib semantic-version==2.5.0 @@ -114,6 +115,7 @@ urllib3==1.14 waitress==0.8.10 WebOb==1.6.0 Werkzeug==0.11.5 +wheel==0.24.0 wrapt==1.10.7 xhtml2pdf==0.0.6 xmltodict==0.10.1 diff --git a/static/js/services/user-service.js b/static/js/services/user-service.js index b205d331f..9f101374f 100644 --- a/static/js/services/user-service.js +++ b/static/js/services/user-service.js @@ -53,6 +53,26 @@ function(ApiService, CookieService, $rootScope, Config) { } } + if (Config.MARKETO_MUNCHKIN_ID && userResponse['marketo_user_hash']) { + associateLeadBody = {'Email': userResponse.email}; + if (window.Munchkin !== undefined) { + try { + Munchkin.munchkinFunction( + 'associateLead', + associateLeadBody, + userResponse['marketo_user_hash'] + ); + } catch (e) { + } + } else { + window.__quay_munchkin_queue.push([ + 'associateLead', + associateLeadBody, + userResponse['marketo_user_hash'] + ]); + } + } + if (window.Raven !== undefined) { try { Raven.setUser({ diff --git a/templates/base.html b/templates/base.html index f860cef37..d5f03c88b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -75,11 +75,17 @@ {% if munchkin_key %}