11Register a single Dash callback that captures all user interactions across
12an instrumented Dash application and routes every genuine event through
17- Register a universal ``ALL``-pattern ``Input`` callback that listens to
18 ``n_clicks``, ``value``, ``data``, ``figure``, and ``selectedData`` on
19 all instrumented components (those whose IDs were rewritten to
20 ``{"event": "<original-id>"}``).
21- Use ``ctx.triggered_prop_ids`` to build the set of ``(original_id,
22 prop_name)`` pairs that actually triggered the current cycle, filtering
23 out components that are non-``None`` but did not change (e.g. a Dropdown
24 holding its default selection).
25- Log each genuine user interaction via
26 ``_dbg("UI", ..., override=True)`` so UI events always appear in the log
27 regardless of domain debug levels.
28- Truncate large event values before logging (``_MAX_EVENT_VALUE_LEN``).
29 For ``figure`` and ``data`` properties, log type + length summary unless
30 ``DEBUG_LEVELS["UI"] >= 9``.
31- Emit a clear L1 diagnostic warning when the required
32 ``dcc.Store(id=REQUIRED_STORE_ID)`` component is absent from
33 ``app.layout`` at registration time.
45- Must only log events that actually triggered the current callback cycle.
46- Must route all log output through ``_dbg()`` with ``override=True``.
47- Requires Dash (``pip install oc_diagnostics[dash]``).
48- Must return ``no_update`` from the callback so the store is not dirtied.
49- Must not raise; logging failures must be silently swallowed.
50- Event values must be truncated to ``_MAX_EVENT_VALUE_LEN`` characters in
51 ``repr()`` form before being passed to ``_dbg()``.
52- A L1 warning must be emitted when ``REQUIRED_STORE_ID`` is not found in
53 ``app.layout`` at ``register_callbacks()`` time.
56from dash
import Input, Output, State, ALL, ctx, no_update
64REQUIRED_STORE_ID =
"debug-settings-data-store"
65"""The ``id`` of the ``dcc.Store`` component that the UI event logger callback
66writes to. This store must be present in the Dash layout before
67``register_callbacks()`` is called; if it is absent Dash will raise a generic
68callback registration error at startup."""
75_MAX_EVENT_VALUE_LEN = 500
76"""Maximum length (in characters) of the ``repr()`` of a logged event value.
78Values whose repr exceeds this limit are truncated and annotated with the
79original length. ``figure`` and ``data`` properties are summarised as
80``<type len=N>`` unless ``DEBUG_LEVELS["UI"] >= 9``.
84_STRUCTURED_PROPS = frozenset({
"figure",
"data"})
92 """Return a log-safe string representation of *value*.
94 For structured properties (``figure``, ``data``) the full repr is only
95 returned when ``DEBUG_LEVELS["UI"] >= 9``; otherwise a concise type +
96 length summary is returned. For all other properties the repr is
97 truncated at ``_MAX_EVENT_VALUE_LEN`` characters.
99 if prop_name
in _STRUCTURED_PROPS:
100 if DEBUG_LEVELS.get(
"UI", 0) < 9:
101 length = len(value)
if isinstance(value, (dict, list))
else "?"
102 return f
"<{type(value).__name__} len={length}> (set UI level=9 for full value)"
105 if len(raw) > _MAX_EVENT_VALUE_LEN:
106 return raw[:_MAX_EVENT_VALUE_LEN] + f
"... [{len(raw)} chars total]"
111 """Recursively search *node* (a Dash layout) for a component with *target_id*.
113 Returns ``True`` as soon as a match is found; stops at depth 100 to
114 avoid performance issues on very large layouts.
116 if _depth > 100
or node
is None:
121 if getattr(node,
"id",
None) == target_id:
123 children = getattr(node,
"children",
None)
124 if isinstance(children, (list, tuple)):
126 if children
is not None:
132 """Emit a L1 warning if ``REQUIRED_STORE_ID`` is absent from ``app.layout``.
134 Silently skips the check when:
135 - ``app.layout`` is ``None`` (layout not yet assigned), or
136 - ``app.layout`` is a callable (resolved at request time).
139 layout = getattr(app,
"layout",
None)
143 "register_callbacks",
144 f
"WARNING: app.layout is None — cannot verify "
145 f
"'{REQUIRED_STORE_ID}' is present. "
146 f
"Add dcc.Store(id='{REQUIRED_STORE_ID}') before calling "
147 f
"register_callbacks().",
157 "register_callbacks",
158 f
"WARNING: '{REQUIRED_STORE_ID}' not found in app.layout — "
159 f
"the UI event logger callback will fail at runtime. "
160 f
"Add dcc.Store(id='{REQUIRED_STORE_ID}', storage_type='memory') "
161 f
"to the layout before calling register_callbacks().",
175 Register the universal UI event logger callback.
177 This callback listens to the following properties on ALL components
178 whose IDs were instrumented into {"event": "<original-id>"}:
186 These cover all interactive Dash components:
187 Buttons, Dropdowns, Inputs, Checklists, Sliders, Stores,
190 Dash requires explicit property names (no ALL wildcard allowed),
191 so we register one Input() per property.
195 The layout must contain ``dcc.Store(id=REQUIRED_STORE_ID, ...)``. A L1
196 warning is emitted at registration time when this component is not found.
200 The ``ui_event_properties`` setting is read once at callback registration
201 time. Changes to ``global_settings.ui_event_properties`` (via config
202 hot-reload or ``set_global_setting()``) will NOT be picked up by an
203 already-registered callback — a full application restart is required.
207 _dbg(
"UI",
"register_callbacks",
"UI event logger registered", override=
True)
210 Output(REQUIRED_STORE_ID,
"data", allow_duplicate=
True),
212 [Input({
"event": ALL}, prop)
for prop
in UI_EVENT_PROPERTIES],
213 State({
"event": ALL},
"id"),
214 prevent_initial_call=
True,
216 def _log_ui_events(*args):
221 n_props = len(UI_EVENT_PROPERTIES)
222 prop_lists = args[:n_props]
223 event_ids = args[n_props]
if len(args) > n_props
else []
225 func =
"_log_ui_events"
228 _dbg(
"UI", func,
"No event_ids received", override=
True)
237 triggered_set: set[tuple[str, str]] = set()
238 for prop_id_str, id_dict
in (ctx.triggered_prop_ids
or {}).items():
239 if "." in prop_id_str
and isinstance(id_dict, dict)
and "event" in id_dict:
240 prop_part = prop_id_str.rsplit(
".", 1)[1]
241 triggered_set.add((id_dict[
"event"], prop_part))
244 for prop_name, values
in zip(UI_EVENT_PROPERTIES, prop_lists):
245 for value, eid
in zip(values, event_ids):
251 original_id = eid.get(
"event")
255 if (original_id, prop_name)
not in triggered_set:
262 f
"EVENT: id={original_id}, property={prop_name}, value={formatted}",
270def ui_event_logger(app):
271 """Convenience wrapper to match existing usage."""
None _check_store_present(app)
bool _find_component_id(node, str target_id, int _depth=0)
str _format_event_value(str prop_name, value)