Skip to content

Commit

Permalink
feat(tray): Add posthog analytics to tray actions (#737)
Browse files Browse the repository at this point in the history
Co-authored-by: Richard Abrich <[email protected]>
  • Loading branch information
KIRA009 and abrichr authored Jun 13, 2024
1 parent 1338fa2 commit dc0b7ea
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 12 deletions.
32 changes: 32 additions & 0 deletions openadapt/alembic/versions/bb25e889ad71_generate_unique_user_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""generate_unique_user_id
Revision ID: bb25e889ad71
Revises: a29b537fabe6
Create Date: 2024-06-11 17:16:28.009900
"""
from uuid import uuid4
import json

from alembic import op
import sqlalchemy as sa

from openadapt.config import config

# revision identifiers, used by Alembic.
revision = "bb25e889ad71"
down_revision = "a29b537fabe6"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
config.UNIQUE_USER_ID = str(uuid4())
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
config.UNIQUE_USER_ID = ""
# ### end Alembic commands ###
2 changes: 1 addition & 1 deletion openadapt/app/dashboard/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def attach_routes(self) -> APIRouter:
self.app.add_api_route("", self.set_settings, methods=["POST"])
return self.app

Category = Literal["api_keys", "scrubbing", "record_and_replay"]
Category = Literal["api_keys", "scrubbing", "record_and_replay", "general"]

@staticmethod
def get_settings(category: Category) -> dict[str, Any]:
Expand Down
15 changes: 15 additions & 0 deletions openadapt/app/dashboard/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client'
import { get } from '@/api'
import posthog from 'posthog-js'
import { PostHogProvider } from 'posthog-js/react'
import { useEffect } from 'react'

if (typeof window !== 'undefined') {
if (process.env.NEXT_PUBLIC_MODE !== "development") {
Expand All @@ -10,8 +12,21 @@ if (typeof window !== 'undefined') {
}
}

async function getSettings(): Promise<Record<string, string>> {
return get('/api/settings?category=general', {
cache: 'no-store',
})
}


export function CSPostHogProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (process.env.NEXT_PUBLIC_MODE !== "development") {
getSettings().then((settings) => {
posthog.identify(settings['UNIQUE_USER_ID'])
})
}
}, [])
if (process.env.NEXT_PUBLIC_MODE === "development") {
return <>{children}</>;
}
Expand Down
32 changes: 26 additions & 6 deletions openadapt/app/tray.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from openadapt.models import Recording
from openadapt.replay import replay
from openadapt.strategies.base import BaseReplayStrategy
from openadapt.utils import get_posthog_instance
from openadapt.visualize import main as visualize

# ensure all strategies are registered
Expand All @@ -51,6 +52,25 @@
ICON_PATH = os.path.join(FPATH, "assets", "logo.png")


class TrackedQAction(QAction):
"""QAction that tracks the recording state."""

def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the TrackedQAction.
Args:
text (str): The text of the action.
parent (QWidget): The parent widget.
"""
super().__init__(*args, **kwargs)
self.triggered.connect(self.track_event)

def track_event(self) -> None:
"""Track the event."""
posthog = get_posthog_instance()
posthog.capture(event="action_triggered", properties={"action": self.text()})


class SystemTrayIcon:
"""System tray icon for OpenAdapt."""

Expand Down Expand Up @@ -94,7 +114,7 @@ def __init__(self) -> None:

self.menu = QMenu()

self.record_action = QAction("Record")
self.record_action = TrackedQAction("Record")
self.record_action.triggered.connect(self._record)
self.menu.addAction(self.record_action)

Expand All @@ -104,15 +124,15 @@ def __init__(self) -> None:
self.populate_menus()

# TODO: Remove this action once dashboard is integrated
# self.app_action = QAction("Show App")
# self.app_action = TrackedQAction("Show App")
# self.app_action.triggered.connect(self.show_app)
# self.menu.addAction(self.app_action)

self.dashboard_action = QAction("Launch Dashboard")
self.dashboard_action = TrackedQAction("Launch Dashboard")
self.dashboard_action.triggered.connect(self.launch_dashboard)
self.menu.addAction(self.dashboard_action)

self.quit = QAction("Quit")
self.quit = TrackedQAction("Quit")

def _quit() -> None:
"""Quit the application."""
Expand Down Expand Up @@ -424,7 +444,7 @@ def populate_menu(self, menu: QMenu, action: Callable, action_type: str) -> None
self.recording_actions[action_type] = []

if not recordings:
no_recordings_action = QAction("No recordings available")
no_recordings_action = TrackedQAction("No recordings available")
no_recordings_action.setEnabled(False)
menu.addAction(no_recordings_action)
self.recording_actions[action_type].append(no_recordings_action)
Expand All @@ -434,7 +454,7 @@ def populate_menu(self, menu: QMenu, action: Callable, action_type: str) -> None
recording.timestamp
).strftime("%Y-%m-%d %H:%M:%S")
action_text = f"{formatted_timestamp}: {recording.task_description}"
recording_action = QAction(action_text)
recording_action = TrackedQAction(action_text)
recording_action.triggered.connect(partial(action, recording))
self.recording_actions[action_type].append(recording_action)
menu.addAction(recording_action)
Expand Down
3 changes: 2 additions & 1 deletion openadapt/config.defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,6 @@
"SAVE_SCREENSHOT_DIFF": false,
"SPACY_MODEL_NAME": "en_core_web_trf",
"DASHBOARD_CLIENT_PORT": 5173,
"DASHBOARD_SERVER_PORT": 8080
"DASHBOARD_SERVER_PORT": 8080,
"UNIQUE_USER_ID": ""
}
5 changes: 5 additions & 0 deletions openadapt/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ def validate_scrub_fill_color(cls, v: Union[str, int]) -> int: # noqa: ANN102

SOM_SERVER_URL: str = "<SOM_SERVER_URL>"

UNIQUE_USER_ID: str = ""

class Adapter(str, Enum):
"""Adapter for the completions API."""

Expand Down Expand Up @@ -285,6 +287,9 @@ def __setattr__(self, key: str, value: Any) -> None:
"RECORD_IMAGES",
"VIDEO_PIXEL_FORMAT",
],
"general": [
"UNIQUE_USER_ID",
],
}


Expand Down
4 changes: 4 additions & 0 deletions openadapt/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def get_events(
Returns:
list: A list of action events.
"""
posthog = utils.get_posthog_instance()
posthog.capture("get_events.started", {"recording_id": recording.id})
start_time = time.time()
action_events = crud.get_action_events(db, recording)
window_events = crud.get_window_events(db, recording)
Expand All @@ -46,6 +48,7 @@ def get_events(
if recording.original_recording_id:
# if recording is a copy, it already has its events processed when it
# was created, return only the top level events
posthog.capture("get_events.completed", {"recording_id": recording.id})
return [event for event in action_events if event.parent_id is None]

raw_action_event_dicts = utils.rows2dicts(action_events)
Expand Down Expand Up @@ -118,6 +121,7 @@ def get_events(
end_time = time.time()
duration = end_time - start_time
logger.info(f"{duration=}")
posthog.capture("get_events.completed", {"recording_id": recording.id})

return action_events # , window_events, screenshots

Expand Down
4 changes: 4 additions & 0 deletions openadapt/replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

LOG_LEVEL = "INFO"

posthog = utils.get_posthog_instance()


@logger.catch
def replay(
Expand All @@ -47,6 +49,7 @@ def replay(
bool: True if replay was successful, None otherwise.
"""
utils.configure_logging(logger, LOG_LEVEL)
posthog.capture("replay.started", {"strategy_name": strategy_name})

if status_pipe:
# TODO: move to Strategy?
Expand Down Expand Up @@ -99,6 +102,7 @@ def replay(

if status_pipe:
status_pipe.send({"type": "replay.stopped"})
posthog.capture("replay.stopped", {"strategy_name": strategy_name, "success": rval})

if record:
sleep(1)
Expand Down
39 changes: 37 additions & 2 deletions openadapt/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@
from jinja2 import Environment, FileSystemLoader
from loguru import logger
from PIL import Image, ImageEnhance
from posthog import Posthog

from openadapt.build_utils import redirect_stdout_stderr
from openadapt.build_utils import is_running_from_executable, redirect_stdout_stderr

with redirect_stdout_stderr():
import fire
Expand All @@ -37,7 +38,12 @@
mss.windows.CAPTUREBLT = 0


from openadapt.config import PERFORMANCE_PLOTS_DIR_PATH, config
from openadapt.config import (
PERFORMANCE_PLOTS_DIR_PATH,
POSTHOG_HOST,
POSTHOG_PUBLIC_KEY,
config,
)
from openadapt.custom_logger import filter_log_messages
from openadapt.db import db
from openadapt.models import ActionEvent
Expand Down Expand Up @@ -658,6 +664,7 @@ def trace(logger: logger) -> Any:
def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper_logging(*args: tuple[tuple, ...], **kwargs: dict[str, Any]) -> Any:
posthog = get_posthog_instance()
func_name = func.__qualname__
func_args = args_to_str(*args)
func_kwargs = kwargs_to_str(**kwargs)
Expand All @@ -666,6 +673,14 @@ def wrapper_logging(*args: tuple[tuple, ...], **kwargs: dict[str, Any]) -> Any:
logger.info(f" -> Enter: {func_name}({func_args}, {func_kwargs})")
else:
logger.info(f" -> Enter: {func_name}({func_args})")
posthog.capture(
event="function_trace",
properties={
"function_name": func_name,
"function_args": func_args,
"function_kwargs": func_kwargs,
},
)

result = func(*args, **kwargs)

Expand Down Expand Up @@ -867,5 +882,25 @@ def split_by_separators(text: str, seps: list[str]) -> list[str]:
return [part for part in parts if part]


class DistinctIDPosthog(Posthog):
"""Posthog client with a distinct ID injected into all events."""

def capture(self, **kwargs: Any) -> None:
"""Capture an event with the distinct ID.
Args:
**kwargs: The event properties.
"""
super().capture(distinct_id=config.UNIQUE_USER_ID, **kwargs)


def get_posthog_instance() -> DistinctIDPosthog:
"""Get an instance of the Posthog client."""
posthog = DistinctIDPosthog(POSTHOG_PUBLIC_KEY, host=POSTHOG_HOST)
if not is_running_from_executable():
posthog.disabled = True
return posthog


if __name__ == "__main__":
fire.Fire(get_functions(__name__))
5 changes: 5 additions & 0 deletions openadapt/visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@
compute_diff,
configure_logging,
evenly_spaced,
get_posthog_instance,
image2utf8,
row2dict,
rows2dicts,
)

SCRUB = config.SCRUB_ENABLED
posthog = get_posthog_instance()

LOG_LEVEL = "INFO"
MAX_EVENTS = None
Expand Down Expand Up @@ -172,6 +174,8 @@ def main(

assert not all([recording, recording_id]), "Only one may be specified."

posthog.capture("visualize.started", {"recording_id": recording_id})

session = crud.get_new_session(read_only=True)

if recording_id:
Expand Down Expand Up @@ -395,6 +399,7 @@ def _cleanup() -> None:

if cleanup:
Timer(1, _cleanup).start()
posthog.capture("visualize.completed", {"recording_id": recording.id})
return True


Expand Down
Loading

0 comments on commit dc0b7ea

Please sign in to comment.