Skip to content

Add Timestamp int64 field to all NATS event structs#59

Merged
Joey0538 merged 11 commits intomainfrom
claude/add-nats-event-timestamps-Sps6H
Apr 8, 2026
Merged

Add Timestamp int64 field to all NATS event structs#59
Joey0538 merged 11 commits intomainfrom
claude/add-nats-event-timestamps-Sps6H

Conversation

@Joey0538
Copy link
Copy Markdown
Collaborator

@Joey0538 Joey0538 commented Apr 8, 2026

Summary

  • Add Timestamp int64 (Unix millis) field to all 8 NATS event structs in pkg/model/event.go for consistent event-level timestamps
  • Update 9 publish sites across 7 services to set Timestamp: time.Now().UTC().UnixMilli() at publish time
  • Change RoomEvent.Timestamp from time.Time to int64 for consistency with the new convention
  • Add "Event Timestamps" rule to CLAUDE.md to enforce the pattern for future events
  • Fix pre-existing hugeParam lint warnings in inbox-worker/handler.go

Motivation

NATS events had inconsistent timestamp coverage — some carried timestamps indirectly via embedded structs, some had explicit time.Time fields, and three had none at all. This makes event ordering, debugging, and observability harder. Every event now has a first-class Timestamp field distinct from domain-level timestamps (e.g., Message.CreatedAt).

Test plan

  • All existing unit tests updated with Timestamp assertions
  • New round-trip tests for NotificationEvent, SubscriptionUpdateEvent, InviteMemberRequest, OutboxEvent, RoomMetadataUpdateEvent
  • make lint — 0 issues
  • make test — all pass

https://claude.ai/code/session_01Qy6cCrGnkHQpzkjbEF8aAv

Summary by CodeRabbit

  • New Features

    • All published events now include a standardized Unix-millisecond "timestamp" representing the event-level publish time.
  • Documentation

    • Added design, plan, and guideline docs describing the timestamp convention and rollout steps.
  • Tests

    • Updated and expanded tests across services to assert presence and non-zero value of event timestamps.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9e0fae31-14b8-4d06-b387-5afcafa728af

📥 Commits

Reviewing files that changed from the base of the PR and between 025ba0e and e67c9da.

📒 Files selected for processing (19)
  • CLAUDE.md
  • broadcast-worker/handler.go
  • broadcast-worker/handler_test.go
  • docs/superpowers/plans/2026-04-08-nats-event-timestamps.md
  • docs/superpowers/specs/2026-04-08-nats-event-timestamps-design.md
  • inbox-worker/handler.go
  • inbox-worker/handler_test.go
  • message-gatekeeper/handler.go
  • message-gatekeeper/handler_test.go
  • notification-worker/handler.go
  • notification-worker/handler_test.go
  • pkg/model/event.go
  • pkg/model/model_test.go
  • pkg/roomkeysender/roomkeysender.go
  • pkg/roomkeysender/roomkeysender_test.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-worker/handler.go
  • room-worker/handler_test.go
✅ Files skipped from review due to trivial changes (5)
  • notification-worker/handler_test.go
  • pkg/roomkeysender/roomkeysender_test.go
  • room-service/handler_test.go
  • broadcast-worker/handler_test.go
  • docs/superpowers/plans/2026-04-08-nats-event-timestamps.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • pkg/roomkeysender/roomkeysender.go
  • message-gatekeeper/handler_test.go
  • room-worker/handler.go
  • CLAUDE.md
  • docs/superpowers/specs/2026-04-08-nats-event-timestamps-design.md
  • pkg/model/model_test.go
  • room-worker/handler_test.go

📝 Walkthrough

Walkthrough

This change adds a top-level Timestamp int64 (Unix milliseconds) to multiple NATS event structs (including converting RoomEvent.Timestamp from time.Time to int64) and updates publishers across services to set the event timestamp at publish time using time.Now().UTC().UnixMilli().

Changes

Cohort / File(s) Summary
Core Model Definition
pkg/model/event.go
Added Timestamp int64 (json:"timestamp" bson:"timestamp") to several event/request types; changed RoomEvent.Timestamp from time.Timeint64.
Model Tests
pkg/model/model_test.go
Updated JSON round-trip fixtures to use millisecond epoch ints and added tests for new event types covering Timestamp.
Broadcast Service
broadcast-worker/handler.go, broadcast-worker/handler_test.go
buildRoomEvent now sets RoomEvent.Timestamp = time.Now().UTC().UnixMilli(); tests assert evt.Timestamp > 0.
Inbox Service
inbox-worker/handler.go, inbox-worker/handler_test.go
Handlers now accept *model.OutboxEvent; handleMemberAdded sets SubscriptionUpdateEvent.Timestamp using UTC Unix millis; tests assert non-zero timestamp.
Message Gatekeeper
message-gatekeeper/handler.go, message-gatekeeper/handler_test.go
processMessage includes Timestamp on published MessageEvent (now.UnixMilli()); tests validate evt.Timestamp > 0.
Notification Service
notification-worker/handler.go, notification-worker/handler_test.go
Set NotificationEvent.Timestamp to current UTC Unix millis; tests assert non-zero timestamp for decoded notifications.
Room Service
room-service/handler.go, room-service/handler_test.go
handleInvite sets InviteMemberRequest.Timestamp, marshals timestamped payload before publishing; test inspects published payload and asserts Timestamp > 0.
Room Worker
room-worker/handler.go, room-worker/handler_test.go
processInvite sets timestamps on OutboxEvent, SubscriptionUpdateEvent, and RoomMetadataUpdateEvent; tests verify each published JSON contains a numeric, non-zero "timestamp".
Room Key Sender
pkg/roomkeysender/roomkeysender.go, pkg/roomkeysender/roomkeysender_test.go
Sender.Send sets evt.Timestamp = time.Now().UTC().UnixMilli() before marshal/publish; test asserts deserialized Timestamp > 0.
Documentation & Policy
CLAUDE.md, docs/superpowers/specs/..., docs/superpowers/plans/...
Added CLAUDE rule and design/plan docs specifying event-level Timestamp convention, required model updates, publisher coordination, and test expectations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • mliu33
  • hmchangw
  • yenta

Poem

🐇 I hop through code at morning's light,
Stamping moments, small and bright,
Unix millis, neat and right,
Events now carry time in flight,
Hooray — the queue clicks into sight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: adding a Timestamp int64 field to NATS event structs. The title is concise, specific, and directly related to the core objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/add-nats-event-timestamps-Sps6H

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

claude added 11 commits April 8, 2026 08:19
Establishes the convention that every NATS event struct gets a
Timestamp int64 field (Unix millis) set at publish time, plus a
CLAUDE.md rule to enforce the pattern for future events.

https://claude.ai/code/session_01Qy6cCrGnkHQpzkjbEF8aAv
10-task TDD plan covering all 8 event structs, 9 publish sites,
CLAUDE.md rule, and full test updates.

https://claude.ai/code/session_01Qy6cCrGnkHQpzkjbEF8aAv
Unmarshal the invite request, stamp it with the current UTC millisecond
timestamp, re-marshal, and publish the timestamped payload to the ROOMS
stream so downstream workers receive an authoritative event time.

https://claude.ai/code/session_01Qy6cCrGnkHQpzkjbEF8aAv
@Joey0538 Joey0538 force-pushed the claude/add-nats-event-timestamps-Sps6H branch from 025ba0e to e67c9da Compare April 8, 2026 08:20
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
pkg/roomkeysender/roomkeysender_test.go (1)

39-81: Add a nil-event test case for Send.

Since Send accepts *model.RoomKeyEvent, please add a table case for evt == nil to lock in error behavior and prevent panic regressions.

Also applies to: 107-107

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/roomkeysender/roomkeysender_test.go` around lines 39 - 81, Add a
table-driven test case where evt is nil to the tests slice used by TestSend (and
the other tests slice at the later occurrence) to assert Send handles a nil
*model.RoomKeyEvent without panicking; in the tests in
pkg/roomkeysender/roomkeysender_test.go add an entry with evt: nil, account set
(e.g., "alice" or blank), publishErr nil, and set wantErr to expect an error (or
an error substring like "room key" or "nil")—then ensure the test checks for a
non-nil error/substring match from Send rather than allowing a panic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/model/model_test.go`:
- Around line 306-317: The test TestNotificationEventJSON fails to compile
because roundTrip uses a [T comparable] constraint while model.NotificationEvent
embeds model.Message with time.Time (non-comparable); replace the roundTrip
usage with an explicit JSON marshal/unmarshal flow: json.Marshal the src
NotificationEvent, json.Unmarshal into a fresh model.NotificationEvent variable,
and compare src vs the unmarshaled value using reflect.DeepEqual (or cmp.Equal)
asserting equality; reference the TestNotificationEventJSON, roundTrip,
model.NotificationEvent, model.Message, json.Marshal/json.Unmarshal, and
reflect.DeepEqual in your change.

In `@pkg/roomkeysender/roomkeysender.go`:
- Around line 28-30: The Send method currently dereferences evt to set
evt.Timestamp and will panic if evt is nil; update Sender.Send to first guard
against a nil evt (e.g., if evt == nil) and return a descriptive error before
touching evt.Timestamp or calling json.Marshal, referencing the Send method and
the RoomKeyEvent/evt variable to locate where to add the check.

---

Nitpick comments:
In `@pkg/roomkeysender/roomkeysender_test.go`:
- Around line 39-81: Add a table-driven test case where evt is nil to the tests
slice used by TestSend (and the other tests slice at the later occurrence) to
assert Send handles a nil *model.RoomKeyEvent without panicking; in the tests in
pkg/roomkeysender/roomkeysender_test.go add an entry with evt: nil, account set
(e.g., "alice" or blank), publishErr nil, and set wantErr to expect an error (or
an error substring like "room key" or "nil")—then ensure the test checks for a
non-nil error/substring match from Send rather than allowing a panic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f3d3f4d-97e2-4985-a09d-15e8b3f18a1c

📥 Commits

Reviewing files that changed from the base of the PR and between 01edddb and 025ba0e.

📒 Files selected for processing (19)
  • CLAUDE.md
  • broadcast-worker/handler.go
  • broadcast-worker/handler_test.go
  • docs/superpowers/plans/2026-04-08-nats-event-timestamps.md
  • docs/superpowers/specs/2026-04-08-nats-event-timestamps-design.md
  • inbox-worker/handler.go
  • inbox-worker/handler_test.go
  • message-gatekeeper/handler.go
  • message-gatekeeper/handler_test.go
  • notification-worker/handler.go
  • notification-worker/handler_test.go
  • pkg/model/event.go
  • pkg/model/model_test.go
  • pkg/roomkeysender/roomkeysender.go
  • pkg/roomkeysender/roomkeysender_test.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-worker/handler.go
  • room-worker/handler_test.go

Comment thread pkg/model/model_test.go
Comment on lines +306 to +317
func TestNotificationEventJSON(t *testing.T) {
src := model.NotificationEvent{
Type: "new_message",
RoomID: "room-1",
Message: model.Message{
ID: "m1", RoomID: "room-1", UserID: "u1", UserAccount: "alice",
Content: "hello", CreatedAt: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
},
Timestamp: 1735689600000,
}
roundTrip(t, &src, &model.NotificationEvent{})
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

roundTrip helper will not compile with NotificationEvent.

The roundTrip helper uses a [T comparable] constraint, but NotificationEvent contains an embedded Message struct with time.Time fields, making it non-comparable in Go. This test will fail to compile.

🐛 Proposed fix: use explicit marshal/unmarshal with reflect.DeepEqual
 func TestNotificationEventJSON(t *testing.T) {
 	src := model.NotificationEvent{
 		Type:   "new_message",
 		RoomID: "room-1",
 		Message: model.Message{
 			ID: "m1", RoomID: "room-1", UserID: "u1", UserAccount: "alice",
 			Content: "hello", CreatedAt: time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC),
 		},
 		Timestamp: 1735689600000,
 	}
-	roundTrip(t, &src, &model.NotificationEvent{})
+	data, err := json.Marshal(&src)
+	require.NoError(t, err)
+	var dst model.NotificationEvent
+	require.NoError(t, json.Unmarshal(data, &dst))
+	if !reflect.DeepEqual(src, dst) {
+		t.Errorf("round-trip mismatch:\n  got  %+v\n  want %+v", dst, src)
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/model/model_test.go` around lines 306 - 317, The test
TestNotificationEventJSON fails to compile because roundTrip uses a [T
comparable] constraint while model.NotificationEvent embeds model.Message with
time.Time (non-comparable); replace the roundTrip usage with an explicit JSON
marshal/unmarshal flow: json.Marshal the src NotificationEvent, json.Unmarshal
into a fresh model.NotificationEvent variable, and compare src vs the
unmarshaled value using reflect.DeepEqual (or cmp.Equal) asserting equality;
reference the TestNotificationEventJSON, roundTrip, model.NotificationEvent,
model.Message, json.Marshal/json.Unmarshal, and reflect.DeepEqual in your
change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False positive — NotificationEvent is comparable in Go. time.Time is a struct containing only primitive fields and a pointer (wall uint64, ext int64, loc *Location), which satisfies the comparable constraint. The test compiles and passes:

$ go test -run TestNotificationEventJSON -v ./pkg/model/...
=== RUN   TestNotificationEventJSON
--- PASS: TestNotificationEventJSON (0.00s)
PASS

No changes needed.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Joey0538, you are correct — I apologize for the false positive. In Go, time.Time satisfies comparable because all its underlying fields (wall uint64, ext int64, loc *Location) support ==, and pointer types are comparable by address. The comparable constraint only requires that == is syntactically valid, not deep structural equality. The test compiles and passes as you demonstrated, so no change is needed.


✏️ Learnings added
Learnt from: Joey0538
Repo: hmchangw/chat PR: 59
File: pkg/model/model_test.go:306-317
Timestamp: 2026-04-08T08:25:48.132Z
Learning: In Go, `time.Time` satisfies the `comparable` constraint (used in generic type parameters) because all of its fields — `wall uint64`, `ext int64`, `loc *Location` — support `==`. Pointer types are comparable by address. Therefore, any struct embedding or containing `time.Time` fields (including `model.Message` and structs embedding it, such as `model.NotificationEvent`) is also `comparable`. Do not flag `roundTrip[T comparable]` usage with types containing `time.Time` as a compilation error.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: hmchangw/chat PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-08T01:57:58.612Z
Learning: Applies to pkg/model/model_test.go : `pkg/model/model_test.go` verifies all domain types marshal/unmarshal correctly via a generic `roundTrip` helper

Learnt from: mliu33
Repo: hmchangw/chat PR: 55
File: pkg/model/room.go:27-32
Timestamp: 2026-04-08T01:06:38.100Z
Learning: When reviewing Go code under pkg/model/, only require both `json` and `bson` tags for MongoDB document structs (the types that are persisted to MongoDB, e.g., `Room`, `Subscription`, `Message`, `User`). For NATS request/response DTOs (e.g., `CreateRoomRequest`, `InviteMemberRequest`) that are not written to MongoDB, it is intentional to have only `json` tags—do not flag missing `bson` tags on these DTOs as a violation.

Learnt from: mliu33
Repo: hmchangw/chat PR: 55
File: pkg/model/event.go:25-29
Timestamp: 2026-04-08T01:06:33.467Z
Learning: In `pkg/model/event.go` (Go), `InviteMemberRequest` is a NATS-only request DTO that is never persisted to MongoDB. The dual `json`+`bson` tag convention (required for MongoDB document structs in `pkg/model/`) does not apply to it. Do not flag missing `bson` tags on `InviteMemberRequest` as an issue.

Learnt from: Joey0538
Repo: hmchangw/chat PR: 52
File: docs/superpowers/specs/2026-04-07-search-sync-worker-design.md:82-82
Timestamp: 2026-04-07T06:13:53.100Z
Learning: In the `hmchangw/chat` repository, the `UpdatedAt time.Time` field (with `json:"updatedAt"` tag) is planned to be added to the `Message` struct in `pkg/model/message.go` as part of implementation step 1 (model changes), as specified in the design doc `docs/superpowers/specs/2026-04-07-search-sync-worker-design.md`. Any constructors, serializers, or helpers that build or handle `Message` should also be updated to populate/handle `UpdatedAt`.

Learnt from: CR
Repo: hmchangw/chat PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-08T01:57:58.612Z
Learning: Applies to pkg/model/**/*.go : `camelCase` for both `json` and `bson` tags, except `_id`

Learnt from: CR
Repo: hmchangw/chat PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-04-08T01:57:58.612Z
Learning: Applies to pkg/model/**/*.go : All model structs get both `json` and `bson` tags

Comment thread pkg/roomkeysender/roomkeysender.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants