Skip to content

Use true UTC timestamps for serialized events #151

Description

@Justinabox

Summary

Public event serialization appends a Z suffix to every event timestamp, but the base Event dataclass defaults to datetime.now() without a timezone. That means normal runtime events created by SMS/USSD/call/modem code are local-time naive datetimes that get labeled as UTC in callstack monitor --json and any future realtime/event-stream consumers using serialize_event().

For unattended Raspberry Pi deployments, this can misorder SMS/call/delivery-report timelines across hosts, log shippers, dashboards, and incident investigations. It also makes the new monitor/realtime foundation less trustworthy before WebSocket/event-feed work expands.

Evidence

Affected code:

  • callstack/events/types.py:15-18 defines Event.timestamp with default_factory=datetime.now, producing a naive local timestamp.
  • callstack/events/serialize.py:33-37 converts aware timestamps to UTC, but for naive timestamps it still returns f"{timestamp.isoformat()}Z".
  • Runtime event constructors such as IncomingSMSEvent(...), USSDResponseEvent(...), RingEvent(...), ModemDisconnectedEvent(...), etc. inherit that default unless callers explicitly pass an aware timestamp.
  • Tests in tests/test_event_serialization.py pass an explicit UTC-aware timestamp, so they do not cover the default runtime path.

No-hardware reproduction on current main (e53d94da997d305a2403322b985b13d8fbff2fce):

PYTHONPATH=. uv run --no-project python -c 'from callstack.events.types import IncomingSMSEvent; from callstack.events.serialize import serialize_event; e=IncomingSMSEvent(sender="+155****0123", body="secret"); print(e.timestamp); print(e.timestamp.tzinfo); print(serialize_event(e)["timestamp"])'

Actual output:

2026-06-27 12:01:08.738956
None
2026-06-27T12:01:08.738956Z

The tzinfo is None, but the serialized value is marked with Z (UTC).

Repository health from this scout run:

git diff --check
# exit 0

PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
# 620 passed in 5.95s

Duplicate checks performed:

gh issue list --repo Justinabox/Callstack --state all --search "UTC timestamp event serialize_event Z naive datetime"
# no matches

gh issue list --repo Justinabox/Callstack --state all --search "monitor timestamp timezone local UTC"
# only broad planning issue #9

gh issue list --repo Justinabox/Callstack --state all --search "datetime.now timezone.utc Event timestamp"
# no matches

gh pr list --repo Justinabox/Callstack --state all --search "timestamp timezone event UTC Z"
# no matches

Expected behavior

Serialized public event timestamps should either:

  1. be truly UTC-aware instants before appending Z, or
  2. avoid claiming UTC for naive local timestamps.

For a PBX/SMS gateway, the preferred behavior is likely UTC-aware event creation by default so local monitor output, future WebSocket events, and persisted/forwarded observability data all share one canonical timeline.

Actual behavior

Default runtime events are naive local datetimes, and serialize_event() labels them as UTC with a Z suffix.

Suggested fix direction

  • Change Event.timestamp to default to an aware UTC value, e.g. datetime.now(timezone.utc).
  • Keep serialize_event() converting aware timestamps to UTC before rendering.
  • Decide how to handle existing caller-provided naive timestamps; fail closed with a clear error in serialization or explicitly treat them as local/UTC with documented behavior. For public event streams, rejecting or normalizing naive timestamps is safer than silently mislabeling them.
  • Add regression coverage for events created without an explicit timestamp, not only explicit timezone.utc fixtures.

Acceptance criteria

  • Default-constructed events serialize with a timestamp that represents the actual UTC instant.
  • serialize_event() no longer appends Z to a naive timestamp without first normalizing/rejecting it deliberately.
  • Tests cover default Event/IncomingSMSEvent timestamp serialization.
  • Existing privacy assertions for SMS bodies, phone-like identifiers, USSD text, and disconnect reasons remain green.

Verification gates

git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/test_event_serialization.py tests/test_cli.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions