Dash App Usage Analytics

I’m usually interested in understanding the usage of my apps, and I use logging for that. I like to know:

  • Are people using Dropdown A or B? Or are they mostly using Slider C?
  • What are the most selected option(s) from those components?
  • How long does it take to actually execute my callbacks?
  • When we moved the Datepicker to the left, did people use it more/less?

This is the first version of a function decorator that does that for any callback (or any other Python function). It takes a timestamp before/after running the function, it gets the module name, function name, and the args with which it was called, and logs them to a file:

def analyze(func):
    def wrapper(*args, **kwargs):
        t0 = datetime.datetime.now()
        result =  func(*args, **kwargs)
        t1 = datetime.datetime.now()
        logging.debug(msg=f'{func.__name__}: [{t0}  {t1}] Ran with: {args}')
        return result
    return wrapper

After defining this function and setting some logging settings, all you have to do is add it as a decorator to the callbacks you want to analyze/track:

@app.callback(Output('single_output', 'children'),
              Input('single_dropdown', 'value'))
@analyze  # <-- that's it
def display_single_day(day):
    return f"You selected {day}"

Running the below sample app and playing with its components a few times produces the following example logs:

[2020-11-25 16:59:14,543] DEBUG in app.display_single_day: [2020-11-25 16:59:14.543916  2020-11-25 16:59:14.543923] Ran with: ('Wed',)
[2020-11-25 16:59:14,545] DEBUG in app.display_multi_color: [2020-11-25 16:59:14.545222  2020-11-25 16:59:14.545227] Ran with: (['blue'],)
[2020-11-25 16:59:20,634] DEBUG in app.display_single_day: [2020-11-25 16:59:20.634769  2020-11-25 16:59:20.634775] Ran with: ('Sun',)
[2020-11-25 16:59:23,890] DEBUG in app.display_single_day: [2020-11-25 16:59:23.890013  2020-11-25 16:59:23.890019] Ran with: ('Tue',)
[2020-11-25 16:59:31,189] DEBUG in app.display_multi_color: [2020-11-25 16:59:31.189847  2020-11-25 16:59:31.189857] Ran with: (['blue', 'green'],)
[2020-11-25 16:59:33,099] DEBUG in app.display_multi_color: [2020-11-25 16:59:33.099130  2020-11-25 16:59:33.099137] Ran with: (['blue', 'green', 'red'],)
[2020-11-25 16:59:34,679] DEBUG in app.display_multi_color: [2020-11-25 16:59:34.679461  2020-11-25 16:59:34.679468] Ran with: (['green', 'red'],)
[2020-11-25 16:59:35,214] DEBUG in app.display_multi_color: [2020-11-25 16:59:35.214155  2020-11-25 16:59:35.214161] Ran with: (['red'],)
[2020-11-25 16:59:37,236] DEBUG in app.display_multi_color: [2020-11-25 16:59:37.236363  2020-11-25 16:59:37.236370] Ran with: (['red', 'yellow'],)
[2020-11-25 16:59:45,214] DEBUG in app.display_single_day: [2020-11-25 16:59:45.214144  2020-11-25 16:59:45.214150] Ran with: ('Sun',)

Since we format those logs the way we want, we can easily extract the relevant information with a regex:

import re
regex = re.compile('\[.*?\] ([A-Z]+) in (.*?)\.(.*?): \[(.*?)  (.*?)\] Ran with: (.*)')
log_lines = []

with open('analytics.log') as file: 
    for line in file: 
        if regex.findall(line): 
            log_lines.append(regex.findall(line)[0])

After putting them in a list, we make a DataFrame summarizing the user interactions (args can definitely be imporoved):

logs_df = pd.DataFrame(log_lines, columns=['level', 'module', 'function', 'start', 'end', 'args'])
logs_df

level module function start end args
DEBUG app display_single_day 2020-11-25 16:59:14.543916 2020-11-25 16:59:14.543923 (‘Wed’,)
DEBUG app display_multi_color 2020-11-25 16:59:14.545222 2020-11-25 16:59:14.545227 ([‘blue’],)
DEBUG app display_single_day 2020-11-25 16:59:20.634769 2020-11-25 16:59:20.634775 (‘Sun’,)
DEBUG app display_single_day 2020-11-25 16:59:23.890013 2020-11-25 16:59:23.890019 (‘Tue’,)
DEBUG app display_multi_color 2020-11-25 16:59:31.189847 2020-11-25 16:59:31.189857 ([‘blue’, ‘green’],)
DEBUG app display_multi_color 2020-11-25 16:59:33.099130 2020-11-25 16:59:33.099137 ([‘blue’, ‘green’, ‘red’],)
DEBUG app display_multi_color 2020-11-25 16:59:34.679461 2020-11-25 16:59:34.679468 ([‘green’, ‘red’],)
DEBUG app display_multi_color 2020-11-25 16:59:35.214155 2020-11-25 16:59:35.214161 ([‘red’],)
DEBUG app display_multi_color 2020-11-25 16:59:37.236363 2020-11-25 16:59:37.236370 ([‘red’, ‘yellow’],)
DEBUG app display_single_day 2020-11-25 16:59:45.214144 2020-11-25 16:59:45.214150 (‘Sun’,)

Below is a full example:

# app.py

import datetime
import logging
import re

import dash
import dash_html_components as html
import dash_core_components as dcc
from dash.dependencies import Output, Input
import pandas as pd

logging.basicConfig(level=logging.DEBUG,
                    filename='analytics.log',
                    format='[%(asctime)s] %(levelname)s in %(module)s.%(message)s')

def analyze(func):
    def wrapper(*args, **kwargs):
        t0 = datetime.datetime.now()
        result =  func(*args, **kwargs)
        t1 = datetime.datetime.now()
        logging.debug(msg=f'{func.__name__}: [{t0}  {t1}] Ran with: {args}')
        return result
    return wrapper

app = dash.Dash(__name__)

app.layout = html.Div([
    dcc.Dropdown(id='single_dropdown',
                 value='Wed',
                 options=[{'label': day, 'value': day}
                          for day in ['Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri']]),
    html.Div(id='single_output'),
    dcc.Dropdown(id='multi_dropdown',
                 multi=True,
                 value=['blue'],
                 options=[{'label': color, 'value': color}
                         for color in ['blue', 'green', 'yellow', 'orange', 'red']]),
    html.Div(id='multi_output'),
])

@app.callback(Output('single_output', 'children'),
              Input('single_dropdown', 'value'))
@analyze
def display_single_day(day):
    return f"You selected {day}"


@app.callback(Output('multi_output', 'children'),
              Input('multi_dropdown', 'value'))
@analyze
def display_multi_color(colors):
    return f"You selected these colors: {', '.join(colors)}"

if __name__ == '__main__':
    app.run_server(debug=True)

Happy to get feedback or suggestions.
Thanks.

2 Likes