feat(#10695): adds interaction tracking service#10786
Conversation
…tive tests for `InteractionTrackingService`
…tive tests for `InteractionTrackingService`
…er. changes event limits
…5-interaction-log
# Conflicts: # webapp/tests/karma/ts/app.component.spec.ts
Signed-off-by: Diana Barsan <barsan@medic.org>
| appliesIf: function () { | ||
| return true; | ||
| appliesIf: function (contact) { | ||
| return contact.contact.role !== 'chw'; |
There was a problem hiding this comment.
This is so a task doesn't get created for the CHW, and instead only for the patients. It saves from needing to explicitly target clicking and completing specific tasks to display the task group page (now any task I click will yield the same result, but if I had a rogue task for the CHW, i would have had to add code to avoid clicking it).
…ng, and update tests
# Conflicts: # webapp/src/ts/modules/tasks/tasks.component.ts # webapp/tests/karma/ts/modules/tasks/tasks.component.spec.ts
…nhance event assertions, and ensure session integrity
|
Hi @sugat009 . I know queues are long and you're probably surprised by this showing up out of the blue. we've been discussing adding a way for tracking user behavior on the task page, to have a framework for evaluating any future changes we might make. this is part of some squad work, and i'm the only dev on the squad. so ... sorry for just dropping this one on you. |
|
No worries @dianabarsan , I ran out of time today with other PRs. Will look into this first thing tomorrow. |
|
No rush @sugat009 . just as long as it makes it by 5.2.0 release :D |
sugat009
left a comment
There was a problem hiding this comment.
A few findings. Most are inline; two below have no specific line to anchor against.
Three of the must fix issues come with failing karma tests I ran locally against the current branch head. The PII narrowing on task.title and the deployment gap on can_track_task_interactions were also verified against the CHT docs and a fresh local instance.
issue (blocking): PR description vs. implemented limit
The PR description says "max 200 sessions/day, max 500 events/session" (~100k events/day implied). The code at interaction-tracking.service.ts:41 is MAX_EVENTS_PER_DAY = 500 total, no session count cap.
Git history:
7fcb31d09setMAX_EVENTS_PER_SESSION = 500(matched the description).f03e9d318switched toMAX_EVENTS_PER_DAY = 2000with anotherEventCountto avoid double counting the current session.e61eafe15("reduce max events to 500") cut to 500 and droppedotherEventCount. Origin of the daily cap halving issue, also where the description drifted.
Combined with the cap halving (see inline comment on interaction-tracking.service.ts:184), the effective ceiling for an active session is ~250. Could you clarify the intended semantics? Options:
- 500 per session + session count cap (matches description).
- 500 per day total (needs the cap halving fix + description update).
- Higher per day + description update + cap halving fix.
chore (non-blocking): Unrelated refactors in the diff
Four bits of work with no functional link to interaction tracking:
webapp/src/ts/app.component.ts:private→private readonlyon 30+ constructor params.tests/utils/sentinel.js+tests/integration/sentinel/schedules/purging.spec.js:waitForPurgeCompletionnow returns the purge log directly.tests/utils/index.js:stringifyParamextracted fromgetRequestUri.tests/e2e/default/tasks/config/tasks-breadcrumbs-config.js:appliesIftightened.
Each is fine on its own; together they inflate the review surface. For future PRs of this size, consider splitting precursor refactors into their own PRs.
# Conflicts: # webapp/tests/karma/ts/modules/tasks/tasks-group.component.spec.ts # webapp/tests/karma/ts/modules/tasks/tasks.component.spec.ts
…imits, and bolster tests for edge cases
… task permissions, and enhance session event validation
…ilters, and navigation events
…ke clock logic, and streamline session event assertions
|
Addresses the 2026-04-27 review. Most of the blocking issues shared a root Architecture rewrite (covers blocking #2, #3, #5, #7)Events are buffered in memory and persisted in batches to a per-day local
Direct fixes from the review
Tests
Deferred
|
…s, persist buffer on visibility changes, and aggregate day rollover without reload
…nd expose on constructor
|
@sugat009 slight nudge here. we might go through some rounds before this is ready, and I really want to ship this is 5.2. appreciate it! |
Oh, I missed this one. Let me take a look. |
sugat009
left a comment
There was a problem hiding this comment.
The architecture rewrite cleanly addresses the 2026-04-27 blockers. CI green, all 47 checks pass.
I also ran this end to end on a local dev instance: rebuilt the bundle, logged in as a CHW user, drove the tasks tab, and inspected the per day IndexedDB and the meta DB. Service initializes, events buffer and persist, aggregate docs land in meta DB. Spot on.
Four inline comments below. None individually block the merge.
| .then(() => this.checkPrivacyPolicy()) | ||
| .then(() => (this.initialisationComplete = true)) | ||
| .then(() => this.initUser()) | ||
| .then(() => this.interactionTrackingService.init()) |
There was a problem hiding this comment.
issue (non-blocking): init() rejection aborts app boot via the global catch at line 324, navigating to /error/503.
The spec at webapp/tests/karma/ts/services/interaction-tracking.service.spec.ts:204 documents the contract: "The error propagates to the caller (so app.component can decide what to do), but the service is left in a safe disabled state." The service already lands safely on internal failure; the call site just needs to honor that:
.then(() => this.interactionTrackingService.init().catch(err => {
console.warn('Interaction tracking disabled', err);
}))| } | ||
|
|
||
| const taskIndex = this.tasksList.indexOf(task); | ||
| this.interactionTrackingService.record('task:open', task.title, String(taskIndex)); |
There was a problem hiding this comment.
issue (non-blocking): task.title is template resolved by rules-engine.service.ts:379, so {{contact.name}} interpolation reaches this record() call. You documented exactly this risk at tasks-sidebar-filter.component.ts:155-157 and stripped it from the filter path, but kept it here per your "Deferred" note in the response above.
Tested locally: an older aggregate in a CHW user's meta DB had ref: "Home visit for {{contact.name}}" recorded literally. In deployments with real contact data, that resolves to the patient name.
suggestion:
- Open a tracking issue and add
// TODO(#xxxxx)at both call sites (here andtasks-group.component.ts:253) so the deferral survives the squash merge. - Either expand the filter comment to acknowledge the asymmetry is intentional, or add a one liner here explaining that the title is used for instance disambiguation.
There was a problem hiding this comment.
I addressed this in the comment:
PII on task.title in tasks-content / tasks-group records.
Because multiple tasks can have the same form-as-action, the only way to disambiguate the > actual task is by using the unique task title.
There was a problem hiding this comment.
Fixing with storing the title key separately, and logging the untranslated version.
| }; | ||
| } | ||
|
|
||
| private aggregateDocId(date: string): string { |
There was a problem hiding this comment.
question: CHT has two existing patterns for date based meta DB doc IDs.
- Telemetry (
telemetry.service.ts:49-58,:85-97): unpadded year/month/day in_id, plus numericyear/month/dayon metadata. The dbt models on the CHT Sync Postgres replica filter ondoc#>>'{metadata,year}'anddoc#>>'{metadata,month}'. - Feedback (
feedback.service.ts:197-201):feedback-${new Date().toISOString()}-${uuid}, sortable on its own.
The aggregate _id here uses telemetry's unpadded shape, but the metadata at lines 388 to 394 only carries the string date — no numeric year/month/day. So this doc is queryable by neither path: _id range scans put October before February, and the dbt pattern that telemetry supports needs metadata.month to exist.
While testing locally I noticed the meta DB already holds aggregates from earlier iterations with interaction-2026-04-24-… (padded) alongside what current code produces (unpadded). Mixed formats already in the same namespace.
Was the telemetry style _id deliberate? If yes, would you mirror its numeric metadata.year/month/day so existing dbt models work the same? If keeping metadata minimal matters, would feedback's ISO 8601 _id be simpler?
There was a problem hiding this comment.
I think the unpadded version was a miss. thanks for flagging it.
…proper zero-padded date formats, and enhance error handling during initialization Signed-off-by: Diana Barsan <barsan@medic.org>
Description
Closes #10695
Adds an opt-in
InteractionTrackingServicethat records user behavior on the tasks tab (list opens/scrolls/leaves, task opens/form submissions/completes/cancels, task-group navigation, task-filter usage) so analysts can study how CHWs work through their queues.Service (
webapp/src/ts/services/interaction-tracking.service.ts)can_track_task_interactionspermission; no-op for users without it.interaction-YYYY-M-D-{user}). Buffer flushes on a 50-event threshold, on session end (navigating away from/tasks), and on the page-levelvisibilitychangelistener wired inapp.component.init()(typically next app load), any per-day DB that isn't "today" is aggregated into a singletype: 'interaction-log'doc in the user's meta DB and the per-day DB is destroyed. The aggregate replicates to the server-side user-meta DB via the normal meta-DB sync._idisinteraction-{date}-{user}-{deviceId}, so each (user, day, device) produces one doc — no day-spanning rows and no per-event rows in the meta store.Tasks integration
tasks.component,tasks-content.component,tasks-group.component,tasks-sidebar-filter.componentall call into the service for their respective events.task_list:scrollis throttled at 2 s.Tasks-group permission
can_view_tasks_groupand gates the/tasks/grouproute on it. Bundled here because tracking the task-group flow only makes sense once that route is permission-controlled.Tests
tests/e2e/default/tasks/interaction-tracking.wdio-spec.js) drives the full flow under a frozen "yesterday" clock so aggregation can be triggered within a single test. The clock is installed as a Date-only stub viabrowser.addInitScript(full fake-timers break zone.js scheduling); each test wipes server-sidetaskdocs and theuser's server meta DB before login so the rules engine regenerates tasks and
waitForAggregateDocdoesn't pick up a prior run's aggregate.AI Disclosure
I used Claude Code for developing and testing. I have made all design decisions and reviewed code for correctness, clarity and alignment with our standards.
Code review checklist
can_view_old_navigationpermission to see the old design. Test it has appropriate design for RTL languages.Compose URLs
If Build CI hasn't passed, these may 404:
License
The software is provided under AGPL-3.0. Contributions to this project are accepted under the same license.