Source code for visualization_toolkit.helpers.dash.analytics

import base64
import json
import os

from dash import dcc, Dash, Input, Output, State

from visualization_toolkit.constants import (
    LIBRARY_PATH,
    JINJA_ENVIRONMENT,
    ATLAS_USER_STORE_ID,
    ATLAS_VISIBILITY_STORE_ID,
    ATLAS_LOCATION_ID,
    ATLAS_DEBUG,
)

DASHBOARD_INTERACTION = "dashboard interaction"


def location_store(
    refresh: str = "callback-nav",
    id: str = ATLAS_LOCATION_ID,
    **kwargs,
) -> dcc.Location:
    """
    Returns a dcc.Location object for the dash app to navigate between pages and access query parameters in callbacks

    :param refresh: Refresh behavior of ``dcc.Location`` when URLs change. Set to ``callback-nav`` which is most suitable for multi-page apps.
    :param id: HTML ID of the location element, defaults to ``ATLAS_LOCATION_ID``.
    :param kwargs: Optional kwargs to pass to the underlying dcc.Location component.
    :return:
    """
    return dcc.Location(
        id=id,
        refresh=refresh,
        **kwargs,
    )


[docs] def user_store( user_id: str = "NOT_SET", account_id: str = "NOT_SET", user_metadata: dict[str, str] = None, account_metadata: dict[str, str] = None, storage_type: str = "memory", id: str = ATLAS_USER_STORE_ID, ) -> dcc.Store: """ Standard object to store user information as it relates to analytics services (ex: pendo). Requires passing in a User ID and Account ID that will then be annotated with any analytics events fired by the dash app. To keep this data available in a dash app session it will be store clientside using the browsers memory or session :param user_id: User ID associated with this browser session, typically an email :param account_id: Account ID associated with the user_id :param user_metadata: Optional metadata for the user in key/value pairs of a dictionary :param account_metadata: Optional metadata for the account in key/value pairs of a dictionary :param storage_type: Optionally control browser side storage behavior. This is passed to the ``dcc.Store`` ``storage_type`` parameter. :param id: Optionally set an HTML ID for this instead of the default. Note when setting this, all other callbacks connected to this ID must be updated to reference the new/non-default value. :return: """ account_metadata = account_metadata or {"type": "NOT_SET"} user_metadata = user_metadata or {} return dcc.Store( id=id, data={ "user": {"id": user_id} | user_metadata, "account": {"id": account_id, "name": None} | account_metadata, }, storage_type=storage_type, )
[docs] def visibility_store( html_ids: list[str] = None, storage_type: str = "memory", id: str = ATLAS_VISIBILITY_STORE_ID, ): """ Initialize a store to keep track of which elements should emit events when they are visible or not visible on the screen. This should be used with ``pendo_visible_elements_callback`` which will access this store to monitor the associated HTML IDs. :param html_ids: List of HTML IDs to initialize the store with. This list should be the list of HTML elements to be tracked for visibility events :param storage_type: :param id: :return: """ html_ids = html_ids or [] return dcc.Store( id=id, data={ "html_ids": html_ids, }, storage_type=storage_type, )
[docs] def control_store( storage_type: str = "memory", id: str = ATLAS_VISIBILITY_STORE_ID, initial_data: list | dict = None, ): """ Initialize a store to keep track of controls and their current values on a page. A separate callback should be implemented to update the control store when any control value changes :param storage_type: :param id: HTML ID to identify store in dash callbacks :param initial_data: Initial state of store, typically a list :return: """ data = initial_data or {} return dcc.Store( id=id, data=data, storage_type=storage_type, )
[docs] def pendo_initialize_callback( app: Dash, user_store_id: str = ATLAS_USER_STORE_ID, debug: bool = ATLAS_DEBUG ): """ Client Side callback to initialize a Pendo session. Once the session is initialized other client-side callbacks to send Pendo events can be executed. This should be used in conjunction with the ``user_store`` component. The data from that component will be used in initializing the Pendo session :param app: Dash app to register the client side callback :param user_store_id: HTML ID of the user store that contains user/account information to initialize Pendo client. Must be associated with a ``dcc.Store`` component. :param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development. :return: """ with open( os.path.join(LIBRARY_PATH, "templates/pendo/initializePendoCallback.js") ) as f: clientside_script = ( JINJA_ENVIRONMENT.from_string(f.read()) .render(parameters={"debug": debug}) .strip() ) app.clientside_callback( clientside_script, Output(user_store_id, "modified_timestamp"), Input(user_store_id, "data"), )
[docs] def pendo_track_click_callback( app: Dash, button_id: str | dict, page: str, feature: str, event: str, additional_properties: dict = None, store_ids: list[str | dict] = None, data_context: str = DASHBOARD_INTERACTION, debug: bool = ATLAS_DEBUG, prevent_initial_call: bool = True, ): """ Client Side callback to emit a click event every time a ``ymc.Button` is clicked. :param app: Dash app to register the client side callback :param button_id: HTML ID of the corresponding button that is interacted with. :param page: Pendo Page ID for the click action :param feature: Pendo Feature ID for the click action :param event: Pendo Event ID for the click action :param additional_properties: Optional key value metadata pairs to pass into the Pendo event :param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events :param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events :param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development. :param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``. :return: """ additional_properties = additional_properties or {} store_ids = store_ids or [] with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackClick.js")) as f: clientside_script = ( JINJA_ENVIRONMENT.from_string(f.read()) .render( parameters={ "debug": debug, "page": page, "feature": feature, "event": event, "additional_properties": json.dumps( additional_properties | {"html_id": button_id} ), "store_ids": _map_stores_to_store_names(store_ids), "data_context": data_context, } ) .strip() ) app.clientside_callback( clientside_script, Input(button_id, "n_clicks"), State(button_id, "id"), *_map_stores_to_callback_state(store_ids), prevent_initial_call=prevent_initial_call, )
[docs] def pendo_track_input_callback( app: Dash, input_id: str | dict, page: str, feature: str, event: str, additional_properties: dict = None, store_ids: list[str | dict] = None, data_context: str = DASHBOARD_INTERACTION, debug: bool = ATLAS_DEBUG, prevent_initial_call: bool = True, ): """ Client Side callback to emit an input event when a ``ymc.TextField` is interacted with. :param app: Dash app to register the client side callback :param input_id: HTML ID of the corresponding textfield component that is interacted with. :param page: Pendo Page ID for the click action :param feature: Pendo Feature ID for the click action :param event: Pendo Event ID for the click action :param additional_properties: Optional key value metadata pairs to pass into the Pendo event :param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events :param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events :param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development. :param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``. :return: """ additional_properties = additional_properties or {} store_ids = store_ids or [] with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackInput.js")) as f: clientside_script = ( JINJA_ENVIRONMENT.from_string(f.read()) .render( parameters={ "debug": debug, "page": page, "feature": feature, "event": event, "additional_properties": json.dumps( additional_properties | {"html_id": input_id} ), "store_ids": _map_stores_to_store_names(store_ids), "data_context": data_context, } ) .strip() ) app.clientside_callback( clientside_script, Input(input_id, "value"), State(input_id, "id"), *_map_stores_to_callback_state(store_ids), prevent_initial_call=prevent_initial_call, )
[docs] def pendo_track_select_callback( app: Dash, select_id: str | dict, page: str, feature: str, event: str, additional_properties: dict = None, store_ids: list[str | dict] = None, is_multi_select: bool = False, data_context: str = DASHBOARD_INTERACTION, debug: bool = ATLAS_DEBUG, prevent_initial_call: bool = True, ): """ Client Side callback to emit a click event every time a ``ymc.Select` or `ymc.MultiSelect` or ``ymc.DatePicker`` options are chosen. :param app: Dash app to register the client side callback :param select_id: HTML ID of the corresponding dropdown component that is interacted with. :param page: Pendo Page ID for the click action :param feature: Pendo Feature ID for the click action :param event: Pendo Event ID for the click action :param additional_properties: Optional key value metadata pairs to pass into the Pendo event :param is_multi_select: Set to ``True`` if a ``ymc.MultiSelect`` is used :param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events :param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events :param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development. :param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``. :return: """ additional_properties = additional_properties or {} store_ids = store_ids or [] with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackSelect.js")) as f: clientside_script = ( JINJA_ENVIRONMENT.from_string(f.read()) .render( parameters={ "debug": debug, "is_multi_select": is_multi_select, "page": page, "feature": feature, "event": event, "additional_properties": json.dumps( additional_properties | {"html_id": select_id} ), "store_ids": _map_stores_to_store_names(store_ids), "data_context": data_context, } ) .strip() ) app.clientside_callback( clientside_script, Input(select_id, "selectedValues" if is_multi_select else "value"), State(select_id, "id"), *_map_stores_to_callback_state(store_ids), prevent_initial_call=prevent_initial_call, )
[docs] def pendo_track_select_range_callback( app: Dash, select_range_id: str | dict, page: str, feature: str, event: str, additional_properties: dict = None, store_ids: list[str | dict] = None, data_context: str = DASHBOARD_INTERACTION, debug: bool = ATLAS_DEBUG, prevent_initial_call: bool = True, ): """ Client Side callback to emit a click event every time a ``ymc.DateRangePicker` options are chosen. :param app: Dash app to register the client side callback :param select_range_id: HTML ID of the corresponding date range or range component that is interacted with. :param page: Pendo Page ID for the click action :param feature: Pendo Feature ID for the click action :param event: Pendo Event ID for the click action :param additional_properties: Optional key value metadata pairs to pass into the Pendo event :param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events :param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events :param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development. :param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``. :return: """ additional_properties = additional_properties or {} store_ids = store_ids or [] with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackRangeSelect.js")) as f: clientside_script = ( JINJA_ENVIRONMENT.from_string(f.read()) .render( parameters={ "debug": debug, "page": page, "feature": feature, "event": event, "additional_properties": json.dumps( additional_properties | {"html_id": select_range_id} ), "store_ids": _map_stores_to_store_names(store_ids), "data_context": data_context, } ) .strip() ) app.clientside_callback( clientside_script, [Input(select_range_id, "startDate"), Input(select_range_id, "endDate")], State(select_range_id, "id"), *_map_stores_to_callback_state(store_ids), prevent_initial_call=prevent_initial_call, )
[docs] def pendo_track_visible_elements_callback( app: Dash, page: str, feature: str, event: str, additional_properties: dict = None, visibility_store_id: str = ATLAS_VISIBILITY_STORE_ID, store_ids: list[str | dict] = None, data_context: str = DASHBOARD_INTERACTION, feature_mapping: dict = None, debug: bool = ATLAS_DEBUG, prevent_initial_call: bool = None, ): """ Client Side callback to emit an event every time the given html element(s) are visible on screen and also when it exits the viewport :param app: Dash app to register the client side callback :param page: Pendo Page ID for the click action :param feature: Pendo Feature ID for the click action :param event: Pendo Event ID for the click action :param additional_properties: Optional key value metadata pairs to pass into the Pendo event :param visibility_store_id: Store ID that contains the list of HTML IDs to be observing. :param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events :param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events :param feature_mapping: Optional mapping if different html IDs that are tracked are mapped to different pendo features. Keys are HTML IDs and the values are the corresponding feature(s). If not specified, the ``feature`` argument is used. :param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development. :param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``None``. :return: """ additional_properties = additional_properties or {} store_ids = store_ids or [] feature_mapping = feature_mapping or {} # Use b64 encoding to hash HTML IDs into JS safe values. # This allows for nested dictionaries for pattern-matching callbacks to be passed client-side encoded_feature_mapping = {} for key, value in feature_mapping.items(): b64_encoded = base64.b64encode(key.encode("utf-8")).decode("utf-8") encoded_feature_mapping[b64_encoded] = value with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackVisibility.js")) as f: clientside_script = ( JINJA_ENVIRONMENT.from_string(f.read()) .render( parameters={ "debug": debug, "page": page, "feature": feature, "event": event, "additional_properties": json.dumps(additional_properties), "store_ids": _map_stores_to_store_names(store_ids), "data_context": data_context, "feature_mapping": json.dumps(encoded_feature_mapping), } ) .strip() ) app.clientside_callback( clientside_script, Output(visibility_store_id, "modified_timestamp"), Input(visibility_store_id, "data"), *_map_stores_to_callback_state(store_ids), prevent_initial_call=prevent_initial_call, )
[docs] def pendo_track_table_sort_callback( app: Dash, table_id: str | dict, page: str, feature: str, event: str, additional_properties: dict = None, store_ids: list[str | dict] = None, data_context: str = DASHBOARD_INTERACTION, debug: bool = ATLAS_DEBUG, prevent_initial_call: bool = True, ): """ Client Side callback to emit a click event every time a ``ymc.DataGrid` is sorted on a column. :param app: Dash app to register the client side callback :param table_id: HTML ID of the corresponding ``ymc.DataGrid`` that is interacted with. :param page: Pendo Page ID for the click action :param feature: Pendo Feature ID for the click action :param event: Pendo Event ID for the click action :param additional_properties: Optional key value metadata pairs to pass into the Pendo event :param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events :param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events :param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development. :param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``. :return: """ additional_properties = additional_properties or {} store_ids = store_ids or [] with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackTableSort.js")) as f: clientside_script = ( JINJA_ENVIRONMENT.from_string(f.read()) .render( parameters={ "debug": debug, "page": page, "feature": feature, "event": event, "additional_properties": json.dumps( additional_properties | {"html_id": table_id} ), "store_ids": _map_stores_to_store_names(store_ids), "data_context": data_context, } ) .strip() ) app.clientside_callback( clientside_script, Input(table_id, "sortModel"), State(table_id, "id"), *_map_stores_to_callback_state(store_ids), prevent_initial_call=prevent_initial_call, )
[docs] def pendo_track_table_filter_callback( app: Dash, table_id: str | dict, page: str, feature: str, event: str, additional_properties: dict = None, store_ids: list[str | dict] = None, data_context: str = DASHBOARD_INTERACTION, debug: bool = ATLAS_DEBUG, prevent_initial_call: bool = True, ): """ Client Side callback to emit a click event every time a ``ymc.DataGrid` is filtered on a column. :param app: Dash app to register the client side callback :param table_id: HTML ID of the corresponding ``ymc.DataGrid`` that is interacted with. :param page: Pendo Page ID for the click action :param feature: Pendo Feature ID for the click action :param event: Pendo Event ID for the click action :param additional_properties: Optional key value metadata pairs to pass into the Pendo event :param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events :param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events :param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development. :param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``True``. :return: """ additional_properties = additional_properties or {} store_ids = store_ids or [] with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackTableFilter.js")) as f: clientside_script = ( JINJA_ENVIRONMENT.from_string(f.read()) .render( parameters={ "debug": debug, "page": page, "feature": feature, "event": event, "additional_properties": json.dumps( additional_properties | {"html_id": table_id} ), "store_ids": _map_stores_to_store_names(store_ids), "data_context": data_context, } ) .strip() ) app.clientside_callback( clientside_script, Input(table_id, "filterModel"), State(table_id, "id"), *_map_stores_to_callback_state(store_ids), prevent_initial_call=prevent_initial_call, )
[docs] def pendo_track_page_load_callback( app: Dash, html_id: str | dict, page: str, feature: str, event: str, additional_properties: dict = None, store_ids: list[str | dict] = None, data_context: str = DASHBOARD_INTERACTION, debug: bool = ATLAS_DEBUG, prevent_initial_call: bool = None, ): """ Client Side callback to emit a load event once the application is fully loaded. This should generally be called once for a data app. :param app: Dash app to register the client side callback :param html_id: HTML ID of the corresponding ``element`` that triggers this callback :param page: Pendo Page ID for the click action :param feature: Pendo Feature ID for the click action :param event: Pendo Event ID for the click action :param additional_properties: Optional key value metadata pairs to pass into the Pendo event :param store_ids: List of optional ``dcc.Store`` IDs to store metadata from in track events :param data_context: Optional metadata to pass into the ``data_context`` field of pendo track events :param debug: When ``True``, the client side callback will log additional information to the browser console. By default, this is ``False`` and should not be used outside of development. :param prevent_initial_call: When ``True``, the client side callback will not be fired when associated component initially loads. Defaults to ``None``. :return: """ additional_properties = additional_properties or {} store_ids = store_ids or [] with open(os.path.join(LIBRARY_PATH, "templates/pendo/trackPageLoad.js")) as f: clientside_script = ( JINJA_ENVIRONMENT.from_string(f.read()) .render( parameters={ "debug": debug, "page": page, "feature": feature, "event": event, "additional_properties": json.dumps( additional_properties | {"html_id": html_id} ), "store_ids": _map_stores_to_store_names(store_ids), "data_context": data_context, } ) .strip() ) app.clientside_callback( clientside_script, Output(html_id, "id"), Input(html_id, "children"), State(html_id, "id"), *_map_stores_to_callback_state(store_ids), prevent_initial_call=prevent_initial_call, )
def _map_stores_to_callback_state(store_ids: list[str]): return [State(_id, "data") for _id in store_ids] def _map_stores_to_store_names(store_ids: list[str | dict]): return json.dumps( [ f"{_id['type']}-{_id['index']}" if isinstance(_id, dict) else _id for _id in store_ids ] )