Merge pull request #123 from lbryio/add-heartbeat

Add basic analytics api and heartbeat event
This commit is contained in:
Jack Robison 2016-08-17 01:34:31 -04:00 committed by GitHub
commit 1932fd72e3
9 changed files with 184 additions and 14 deletions

View file

@ -0,0 +1,2 @@
from events import *
from api import AnalyticsApi as Api

71
lbrynet/analytics/api.py Normal file
View file

@ -0,0 +1,71 @@
import functools
import json
import logging
from requests import auth
from requests_futures import sessions
from lbrynet import conf
from lbrynet.analytics import utils
log = logging.getLogger(__name__)
def log_response(fn):
def _log(future):
if future.cancelled():
log.warning('Request was unexpectedly cancelled')
else:
response = future.result()
log.debug('Response (%s): %s', response.status_code, response.content)
@functools.wraps(fn)
def wrapper(*args, **kwargs):
future = fn(*args, **kwargs)
future.add_done_callback(_log)
return future
return wrapper
class AnalyticsApi(object):
def __init__(self, session, url, write_key):
self.session = session
self.url = url
self.write_key = write_key
@property
def auth(self):
return auth.HTTPBasicAuth(self.write_key, '')
@log_response
def batch(self, events):
"""Send multiple events in one request.
Each event needs to have its type specified.
"""
data = json.dumps({
'batch': events,
'sentAt': utils.now(),
})
log.debug('sending %s events', len(events))
log.debug('Data: %s', data)
return self.session.post(self.url + '/batch', json=data, auth=self.auth)
@log_response
def track(self, event):
"""Send a single tracking event"""
log.debug('Sending track event: %s', event)
import base64
return self.session.post(self.url + '/track', json=event, auth=self.auth)
@classmethod
def load(cls, session=None):
"""Initialize an instance using values from lbry.io."""
if not session:
session = sessions.FuturesSession()
return cls(
session,
conf.ANALYTICS_ENDPOINT,
utils.deobfuscate(conf.ANALYTICS_TOKEN)
)

View file

@ -0,0 +1,47 @@
from lbrynet.analytics import utils
class Events(object):
def __init__(self, context, lbry_id, session_id):
self.context = context
self.lbry_id = lbry_id
self.session_id = session_id
def heartbeat(self):
return {
'userId': 'lbry',
'event': 'Heartbeat',
'properties': {
'lbry_id': self.lbry_id,
'session_id': self.session_id
},
'context': self.context,
'timestamp': utils.now()
}
def make_context(platform, wallet, is_dev=False):
# TODO: distinguish between developer and release instances
return {
'is_dev': is_dev,
'app': {
'name': 'lbrynet',
'version': platform['lbrynet_version'],
'ui_version': platform['ui_version'],
'python_version': platform['python_version'],
'wallet': {
'name': wallet,
# TODO: add in version info for lbrycrdd
'version': platform['lbryum_version'] if wallet == 'lbryum' else None
},
},
# TODO: expand os info to give linux/osx specific info
'os': {
'name': platform['os_system'],
'version': platform['os_release']
},
'library': {
'name': 'lbrynet-analytics',
'version': '1.0.0'
},
}

View file

@ -0,0 +1,8 @@
import datetime
from lbrynet.core.utils import *
def now():
"""Return utc now in isoformat with timezone"""
return datetime.datetime.utcnow().isoformat() + 'Z'

View file

@ -59,4 +59,7 @@ CURRENCIES = {
'USD': {'type': 'fiat'}, 'USD': {'type': 'fiat'},
} }
LOGGLY_TOKEN = 'YWRmNGU4NmEtNjkwNC00YjM2LTk3ZjItMGZhODM3ZDhkYzBi' LOGGLY_TOKEN = 'LJEzATH4AzRgAwxjAP00LwZ2YGx3MwVgZTMuBQZ3MQuxLmOv'
ANALYTICS_ENDPOINT = 'https://api.segment.io/v1'
ANALYTICS_TOKEN = 'Ax5LZzR1o3q3Z3WjATASDwR5rKyHH0qOIRIbLmMXn2H='

View file

@ -1,12 +1,14 @@
import base64
import json import json
import logging import logging
import logging.handlers import logging.handlers
import sys import sys
import traceback import traceback
from requests_futures.sessions import FuturesSession
import lbrynet import lbrynet
from lbrynet import conf from lbrynet import conf
from requests_futures.sessions import FuturesSession from lbrynet.core import utils
session = FuturesSession() session = FuturesSession()
@ -95,7 +97,7 @@ def configure_file_handler(file_name, **kwargs):
def get_loggly_url(token=None, version=None): def get_loggly_url(token=None, version=None):
token = token or base64.b64decode(conf.LOGGLY_TOKEN) token = token or utils.deobfuscate(conf.LOGGLY_TOKEN)
version = version or lbrynet.__version__ version = version or lbrynet.__version__
return LOGGLY_URL.format(token=token, tag='lbrynet-' + version) return LOGGLY_URL.format(token=token, tag='lbrynet-' + version)

View file

@ -1,3 +1,4 @@
import base64
import distutils.version import distutils.version
import random import random
@ -37,3 +38,11 @@ def version_is_greater_than(a, b):
return distutils.version.StrictVersion(a) > distutils.version.StrictVersion(b) return distutils.version.StrictVersion(a) > distutils.version.StrictVersion(b)
except ValueError: except ValueError:
return distutils.version.LooseVersion(a) > distutils.version.LooseVersion(b) return distutils.version.LooseVersion(a) > distutils.version.LooseVersion(b)
def deobfuscate(obfustacated):
return base64.b64decode(obfustacated.decode('rot13'))
def obfuscate(plain):
return base64.b64encode(plain).encode('rot13')

View file

@ -26,6 +26,7 @@ from txjsonrpc.web.jsonrpc import Handler
from lbrynet import __version__ as lbrynet_version from lbrynet import __version__ as lbrynet_version
from lbryum.version import LBRYUM_VERSION as lbryum_version from lbryum.version import LBRYUM_VERSION as lbryum_version
from lbrynet import analytics
from lbrynet.core.PaymentRateManager import PaymentRateManager from lbrynet.core.PaymentRateManager import PaymentRateManager
from lbrynet.core.server.BlobAvailabilityHandler import BlobAvailabilityHandlerFactory from lbrynet.core.server.BlobAvailabilityHandler import BlobAvailabilityHandlerFactory
from lbrynet.core.server.BlobRequestHandler import BlobRequestHandlerFactory from lbrynet.core.server.BlobRequestHandler import BlobRequestHandlerFactory
@ -174,6 +175,7 @@ class LBRYDaemon(jsonrpc.JSONRPC):
self.known_dht_nodes = KNOWN_DHT_NODES self.known_dht_nodes = KNOWN_DHT_NODES
self.first_run_after_update = False self.first_run_after_update = False
self.uploaded_temp_files = [] self.uploaded_temp_files = []
self._session_id = base58.b58encode(generate_id())
if os.name == "nt": if os.name == "nt":
from lbrynet.winhelpers.knownpaths import get_path, FOLDERID, UserHandle from lbrynet.winhelpers.knownpaths import get_path, FOLDERID, UserHandle
@ -510,6 +512,7 @@ class LBRYDaemon(jsonrpc.JSONRPC):
d.addCallback(lambda _: threads.deferToThread(self._setup_data_directory)) d.addCallback(lambda _: threads.deferToThread(self._setup_data_directory))
d.addCallback(lambda _: self._check_db_migration()) d.addCallback(lambda _: self._check_db_migration())
d.addCallback(lambda _: self._get_settings()) d.addCallback(lambda _: self._get_settings())
d.addCallback(lambda _: self._set_events())
d.addCallback(lambda _: self._get_session()) d.addCallback(lambda _: self._get_session())
d.addCallback(lambda _: add_lbry_file_to_sd_identifier(self.sd_identifier)) d.addCallback(lambda _: add_lbry_file_to_sd_identifier(self.sd_identifier))
d.addCallback(lambda _: self._setup_stream_identifier()) d.addCallback(lambda _: self._setup_stream_identifier())
@ -519,21 +522,33 @@ class LBRYDaemon(jsonrpc.JSONRPC):
d.addCallback(lambda _: self._setup_server()) d.addCallback(lambda _: self._setup_server())
d.addCallback(lambda _: _log_starting_vals()) d.addCallback(lambda _: _log_starting_vals())
d.addCallback(lambda _: _announce_startup()) d.addCallback(lambda _: _announce_startup())
d.addCallback(lambda _: self._load_analytics_api())
# TODO: handle errors here
d.callback(None) d.callback(None)
return defer.succeed(None) return defer.succeed(None)
def _load_analytics_api(self):
self.analytics_api = analytics.Api.load()
self.send_heartbeat = LoopingCall(self._send_heartbeat)
self.send_heartbeat.start(60)
def _send_heartbeat(self):
log.debug('Sending heartbeat')
heartbeat = self._events.heartbeat()
self.analytics_api.track(heartbeat)
def _get_platform(self): def _get_platform(self):
r = { r = {
"processor": platform.processor(), "processor": platform.processor(),
"python_version: ": platform.python_version(), "python_version": platform.python_version(),
"platform": platform.platform(), "platform": platform.platform(),
"os_release": platform.release(), "os_release": platform.release(),
"os_system": platform.system(), "os_system": platform.system(),
"lbrynet_version: ": lbrynet_version, "lbrynet_version": lbrynet_version,
"lbryum_version: ": lbryum_version, "lbryum_version": lbryum_version,
"ui_version": self.lbry_ui_manager.loaded_git_version, "ui_version": self.lbry_ui_manager.loaded_git_version,
} }
if not self.ip: if not self.ip:
try: try:
r['ip'] = json.load(urlopen('http://jsonip.com'))['ip'] r['ip'] = json.load(urlopen('http://jsonip.com'))['ip']
@ -545,13 +560,16 @@ class LBRYDaemon(jsonrpc.JSONRPC):
def _initial_setup(self): def _initial_setup(self):
def _log_platform(): def _log_platform():
log.info("Platform: " + json.dumps(self._get_platform())) log.info("Platform: %s", json.dumps(self._get_platform()))
return defer.succeed(None) return defer.succeed(None)
d = _log_platform() d = _log_platform()
return d return d
def _set_events(self):
context = analytics.make_context(self._get_platform(), self.wallet_type)
self._events = analytics.Events(context, base58.b58encode(self.lbryid), self._session_id)
def _check_network_connection(self): def _check_network_connection(self):
try: try:
host = socket.gethostbyname(REMOTE_SERVER) host = socket.gethostbyname(REMOTE_SERVER)
@ -939,10 +957,9 @@ class LBRYDaemon(jsonrpc.JSONRPC):
return d return d
def _modify_loggly_formatter(self): def _modify_loggly_formatter(self):
session_id = base58.b58encode(generate_id())
log_support.configure_loggly_handler( log_support.configure_loggly_handler(
lbry_id=base58.b58encode(self.lbryid), lbry_id=base58.b58encode(self.lbryid),
session_id=session_id session_id=self._session_id
) )

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from lbrynet.core import utils from lbrynet.core import utils
from twisted.trial import unittest from twisted.trial import unittest
@ -17,3 +18,13 @@ class CompareVersionTest(unittest.TestCase):
self.assertTrue(utils.version_is_greater_than('1.3.9.1', '1.3.9')) self.assertTrue(utils.version_is_greater_than('1.3.9.1', '1.3.9'))
class ObfuscationTest(unittest.TestCase):
def test_deobfuscation_reverses_obfuscation(self):
plain = "my_test_string"
obf = utils.obfuscate(plain)
self.assertEqual(plain, utils.deobfuscate(obf))
def test_can_use_unicode(self):
plain = ''
obf = utils.obfuscate(plain)
self.assertEqual(plain, utils.deobfuscate(obf))