forked from LBRYCommunity/lbry-sdk
Merge pull request #123 from lbryio/add-heartbeat
Add basic analytics api and heartbeat event
This commit is contained in:
commit
1932fd72e3
9 changed files with 184 additions and 14 deletions
2
lbrynet/analytics/__init__.py
Normal file
2
lbrynet/analytics/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
from events import *
|
||||||
|
from api import AnalyticsApi as Api
|
71
lbrynet/analytics/api.py
Normal file
71
lbrynet/analytics/api.py
Normal 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)
|
||||||
|
)
|
47
lbrynet/analytics/events.py
Normal file
47
lbrynet/analytics/events.py
Normal 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'
|
||||||
|
},
|
||||||
|
}
|
8
lbrynet/analytics/utils.py
Normal file
8
lbrynet/analytics/utils.py
Normal 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'
|
|
@ -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='
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in a new issue