Skip to content

Cycling / activity-trip diagnostics

Read-only tooling to pull and decrypt stored cycling trips so you can investigate why one was rejected, missed, or flagged by user feedback.

The gamification_private.activity_trip columns locations, activity_distribution and user_feedback are Fernet-encrypted, so they can only be read inside the backend app env (needs the Secrets Manager key + a DB session). Two Flask commands expose that: - export_activity_trips — dumps decrypted trips as a JSON array to stdout (feed the native replay harness, or save for offline inspection). - diagnose_activity_trips — fetch + decrypt + analyze in one step, printing a readable report per trip (verdict, GPS speed/noise profile, missing-ride logs). Same filters; no file round-trip.

Two ways to read the data afterwards: - diagnose_activity_trips (above) or analyze_activity_trip.py (here, on an exported JSON) — diagnose one trip so a human or agent can see why it was rejected or missed. Both share one formatter, so the report is identical. - Native replay harness (frontend/.../tools/cycling-replay) — validate algorithm changes by re-running the real on-device code against past trips (better/same/worse vs stored + feedback). Its README has the end-to-end workflow (fetch, diagnose, validate).

Running it (against prod)

# Safest first check: counts + latest trip's plaintext fields, no decryption:
alan qovery run backend-fr--prod "flask gamification export_activity_trips --probe"

# Diagnose a trip in one step (fetch + decrypt + analyze, no file):
alan qovery run backend-fr--prod "flask gamification diagnose_activity_trips --external-id ABC"

# Export decrypted trips as JSON (e.g. to feed the replay harness or inspect offline):
alan qovery run backend-fr--prod "flask gamification export_activity_trips --comment not_bike" > not_bike.json

# Specific trips:
alan qovery run backend-fr--prod \
  "flask gamification export_activity_trips --external-id ABC --external-id DEF" > some.json

Both commands share the filters: --external-id (repeatable), --status, --review-status, --only-with-feedback, --comment <code>, --limit. export_activity_trips also has --probe.

Offline (on an already-exported JSON, no prod): python3 analyze_activity_trip.py trips.json.

Notes: - Read-only by construction: every query runs inside read_only_db_context() (read-only on the primary; writes can't commit). Don't pass a replica lag: the replica lacks grants on gamification_private. - Output goes to stdout (files written inside the qovery container aren't retrievable). - Decryption degrades gracefully: a blob that can't be decrypted comes back as null/[] with a logged warning.

Bundle shape (per trip)

{
  "externalId": "...", "platform": "ios",
  "status": "rejected", "reviewStatus": "pending", "rejectionReason": "unrealistic_avg_speed",
  "summary": { "distanceMeters": 1971, "startTime": "...", "endTime": "...",
               "durationSeconds": 381, "locationCount": 97, "activityType": "cycling" },
  "extraProperties": { "algorithm_version": "0.7.0", "recording_trigger": "..." },
  "activityDistribution": { "cycling": 0.8, "walking": 0.2 },  // CoreMotion ratios; empty {} on trips recorded before this change (not backfillable)
  "locations": [ { "latitude": 0, "longitude": 0, "speed": 0, "horizontal_accuracy": 0, "timestamp": 0, "activity": "cycling" } ],
  "feedback": [ { "submitted_at": "...", "comment": "missing_start_end", "payload": { /* incl. diagnostic */ } } ],
  "diagnostic": null  // the feedback's diagnostic bundle (missing_start_end / ride_not_detected / not_bike), or null
}

Privacy

Exported JSON contains raw GPS tracks and device info. Keep it local; do not commit sample exports.