Relationships
#501 username_metrics projection backfill does not trigger re-scoring (stale UserScore for dormant users)
Opened by keeb · 5/31/2026
Summary
The one-time username_metrics projection backfill introduced in #491 corrects the telemetry projection but never drives a score refresh. As a result, users whose pre-association history was credited by the backfill but who have had no authenticated activity since the deploy keep a stale UserScore (canonical/activity score), even though the corrected data is already in username_metrics.
Background
#491 made username_metrics a materialized projection of identity_map -> user_metrics, folding a device's pre-association history into its owning account. A one-time backfill recomputes every existing mapping at startup.
There are two distinct artifacts:
- The telemetry projection (
username_metrics), read live byGET /v1/stats?username=. The profile's livestatsblock reads this directly, so it reflects the correction immediately. - The stored
UserScorein the main app (canonicalScore,activityScore,scoreSources,scoreHistory). This is written only when a score refresh runs — it is not recomputed at read time.
Root cause
The only trigger for a score refresh is the telemetry watcher's onBatchDelivered -> writeToSwampClubQueue (services/telemetry/lib/queue-producer.ts), which builds its username set from events present in the ingested batch (if (event.username) usernames.add(...)) and publishes a score-refresh message the main app consumes (refreshUserScoreFromApi).
The backfill (backfillUsernameMetricsProjection in services/telemetry/lib/consumers/stats.ts) calls recomputeUsernameMetricsProjection only — it writes username_metrics and publishes nothing to the score queue. The main app's own one-time runUserScoreBackfill is migration-gated and does not re-run on restart. So nothing re-scores the backfilled population.
Impact by cohort
- Active users: score self-heals — each ongoing authenticated event enqueues a refresh that reads the corrected projection.
- On-bind (a dormant user runs one authenticated command): correct — the binding event carries a username, so it is in the batch and triggers a refresh.
- Backfilled + now dormant: projection is correct, but
UserScoreis stale until the user's next authenticated event (a CLI command, or even a web sign-in). The livestatsblock masks this because it reads the projection directly, not the stored score.
Sibling gap
A steady-state anonymous event on an already-bound device updates the projection (via the ingest recompute) but does not enqueue a refresh either, because the batch carries no username. The score lags until the next authenticated event for that user.
Proposed fix
After the backfill recomputes a username's projection, publish a score-refresh message for that username — reuse the same scoreRefreshQueue the producer uses, batched over the recomputed usernames. The main app then re-scores from the corrected projection with no new event required. This is the missing second half of the one-time backfill (data half landed in #491, score-propagation half did not). Optionally also enqueue a refresh from the ingest recompute path when an anonymous event updates an already-bound username, to close the sibling gap.
Affected
services/telemetry/lib/consumers/stats.ts—backfillUsernameMetricsProjection, the ingest recompute pathservices/telemetry/lib/queue-producer.ts— score-refresh publish (reuse)
Related
Follow-up to #491.
Closed
No activity in this phase yet.
Sign in to post a ripple.