Skip to content

feat(detections): add recorded_at field for capture-time sequence linking#617

Open
Acruve15 wants to merge 5 commits into
mainfrom
alexis/510-detection-recorded-at
Open

feat(detections): add recorded_at field for capture-time sequence linking#617
Acruve15 wants to merge 5 commits into
mainfrom
alexis/510-detection-recorded-at

Conversation

@Acruve15

Copy link
Copy Markdown
Collaborator

Closes #510

Motivation

The POST /detections endpoint imposed created_at = utcnow(), so detections cached on the engine and uploaded in a burst all landed at virtually the same DB timestamp, even though they were captured minutes or hours apart. Sequence linking buckets detections by time, so a burst upload either grouped unrelated captures or split a real sequence depending on upload timing.

This adds a recorded_at field carrying the on-device capture time (UTC) and switches all sequence linking and time-window logic to use it. created_at stays as the trusted DB insertion clock.

Per the issue thread (frgfm / fe51 / MateoLostanlen): both fields are kept on purpose. created_at is the timestamp we can always trust; the created_at - recorded_at gap is a useful signal for spotting cameras with connection lag. The EXIF-extraction idea was considered and deferred (not all cameras expose it, TZ handling, latency) in favor of the engine sending recorded_at explicitly.

What changed

  • Model / schema: Detection.recorded_at (naive-UTC, default_factory=utcnow), optional recorded_at on DetectionCreate. Read endpoints (/detections/{id}, /detections/, /sequences/{id}/detections) expose it for free via the inherited read schemas.
  • Endpoint (src/app/api/api_v1/endpoints/detections.py): recorded_at is an optional form field defaulting to server now when the engine omits it (so un-updated engines keep working, and the envdenv "alerts in the past" demo just needs a past recorded_at). Sequence candidate lookup, the overlap window, started_at / last_seen_at, and the last-bbox lookup all key on recorded_at; the time-window comparisons anchor on the new detection's capture time instead of utcnow(), so a late upload attaches to the sequence that was active at capture time.
  • Sequences (src/app/api/api_v1/endpoints/sequences.py): GET /sequences/{id}/detections orders by recorded_at.
  • UTC normalization: a to_utc_naive helper (next to utcnow()) converts timezone-aware payloads (e.g. a France-local +02:00 value) to UTC before storing and before the window comparisons; naive values are assumed UTC. Coercion rather than rejection, so a tz-aware timestamp does the right thing instead of erroring.
  • Migration: adds the column as nullable, backfills recorded_at = created_at for existing rows, then sets NOT NULL.

alerts.py and the fromdate endpoints need no change: they filter on started_at / last_seen_at, which now derive from recorded_at transitively.

Tests

  • New: payload value honored, tz-aware value converted to UTC, default-to-now, recorded_at-keyed grouping, distant captures don't group.
  • Full suite green (503 passed) in the dockerized stack; migration verified up + down + re-up with the column landing as NOT NULL.

Acruve15 added 3 commits June 10, 2026 10:50
Add a recorded_at timestamp to the detections table, carrying the
on-device capture time (UTC), distinct from created_at which stays as
the trusted DB insertion clock. New rows default recorded_at to
utcnow(). The Alembic migration adds the column as nullable, backfills
it from created_at for existing rows, then tightens it to NOT NULL.

Refs #510
Sequence bucketing now keys on each detection's recorded_at instead of
created_at, so detections cached on the engine and uploaded in a burst
group by when they were captured rather than when they reached the API.
The POST /detections payload accepts an optional recorded_at (defaults
to server now when the engine omits it), and the candidate-sequence and
overlap time-window queries anchor on the new detection's capture time
instead of utcnow(). Sequence started_at / last_seen_at and the
GET /sequences/{id}/detections ordering follow recorded_at too.

Refs #510
Timezone-aware capture timestamps (for example a France-local +02:00
value) are converted to UTC before being stored and before the sequence
time-window comparisons, matching the naive-UTC convention used by the
rest of the schema. Naive timestamps are assumed to already be UTC. Adds
a to_utc_naive helper next to utcnow().

Refs #510
Comment thread src/migrations/versions/2026_05_27_1000-c4e9f1a2b3d5_add_recorded_at_to_detections.py Dismissed
Comment thread src/migrations/versions/2026_05_27_1000-c4e9f1a2b3d5_add_recorded_at_to_detections.py Dismissed
Comment thread src/migrations/versions/2026_05_27_1000-c4e9f1a2b3d5_add_recorded_at_to_detections.py Dismissed
Comment thread src/migrations/versions/2026_05_27_1000-c4e9f1a2b3d5_add_recorded_at_to_detections.py Dismissed
Resolve conflicts in the sequence detections endpoint and the detection
tests, where both branches appended independent code (crop image support
from #575 alongside recorded_at). The sequence endpoint now orders by
recorded_at while keeping the new offset / with_crop params.

Re-point the recorded_at migration onto the crop_bucket_key migration so
the Alembic history stays a single linear head.
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.32%. Comparing base (00743b2) to head (25edadc).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #617      +/-   ##
==========================================
+ Coverage   92.30%   92.32%   +0.02%     
==========================================
  Files          56       56              
  Lines        2585     2593       +8     
==========================================
+ Hits         2386     2394       +8     
  Misses        199      199              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The sequence started_at now derives from the detection recorded_at
rather than created_at, which come from separate utcnow() calls. Compare
against recorded_at so the end-to-end check matches the new behavior.

Refs #510
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Detection: Being able to set the recorded_at field when recording a detection.

1 participant