refactor(cloud): treat alert ids as opaque strings (#4924)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Amir Raminfar
2026-08-16 19:35:15 -07:00
committed by GitHub
co-authored by Claude Opus 5
parent 82dea58f40
commit 11d2894f04
9 changed files with 41 additions and 26 deletions
@@ -17,7 +17,7 @@ const ns = (n: number) => n * 1_000_000;
function mountAlert(overrides: Partial<CloudAlert> = {}) {
const alert: CloudAlert = {
alertId: 1,
alertId: "6teOh3RH",
containerId: "abc",
hostId: "h",
ts: ns(1_000_000),
+4 -4
View File
@@ -23,7 +23,7 @@ function log(id: number, at: number, containerID = "abc"): LogEntry<LogMessage>
function alert(overrides: Partial<CloudAlert> = {}): CloudAlert {
return {
alertId: 1,
alertId: "1",
containerId: "abc",
hostId: "h",
ts: ns(100),
@@ -91,7 +91,7 @@ describe("mergeAlerts", () => {
const logs = [log(10, 500)];
const merged = mergeAlerts(
logs,
[alert({ alertId: 2, ts: ns(300) }), alert({ alertId: 1, ts: ns(100) })],
[alert({ alertId: "2", ts: ns(300) }), alert({ alertId: "1", ts: ns(100) })],
new Set(),
);
@@ -138,7 +138,7 @@ describe("mergeAlerts", () => {
const logs = [log(10, 100)];
const merged = mergeAlerts(
logs,
[alert({ alertId: 1, isOrigin: false, ts: ns(50) }), alert({ alertId: 1, isOrigin: true, ts: ns(100) })],
[alert({ alertId: "1", isOrigin: false, ts: ns(50) }), alert({ alertId: "1", isOrigin: true, ts: ns(100) })],
new Set(),
);
expect(shapeOf(merged)).toEqual(["alert:1", "log:10"]);
@@ -236,7 +236,7 @@ describe("attachEvents", () => {
// badging its line too would say the same thing twice.
test("does not badge a line whose event produced an alert", () => {
const logs = [log(10, 100)];
expect(attachEvents(logs, [event({ suppressed: false, alertId: 7 })])).toBe(false);
expect(attachEvents(logs, [event({ suppressed: false, alertId: "7" })])).toBe(false);
expect(logs[0].matchedEvent).toBeUndefined();
});
+4 -2
View File
@@ -6,7 +6,8 @@ import { AlertLogEntry, CloudEventLogEntry, LogEntry, type LogMessage } from "@/
* /api/cloud/alerts. Mirrors cloud.AlertHit on the Go side.
*/
export interface CloudAlert {
alertId: number;
/** Opaque, sqids-encoded. Never a raw row id. */
alertId: string;
containerId: string;
hostId: string;
/**
@@ -49,7 +50,8 @@ export interface CloudEvent {
type?: string;
/** Human-readable summary; the only renderable text a non-log event has. */
detail?: string;
alertId?: number;
/** Opaque id of the alert this event reached, empty when it reached none. */
alertId?: string;
suppressed: boolean;
}
+1 -1
View File
@@ -15,7 +15,7 @@ export function useLogLoader(
) {
const { fetchAlerts, available: alertsAvailable } = useCloudAlerts();
// Anchor keys already placed, so overlapping scroll windows don't duplicate.
// Keyed on (alertId, anchor) rather than alertId: one incident legitimately
// Keyed on (alert, anchor) rather than the alert alone: one incident legitimately
// marks every window it was active in.
const placedAlerts = new Set<string>();
// Newest log timestamp the poll has already asked Cloud about. Events only
+5 -2
View File
@@ -227,7 +227,10 @@ export class AlertLogEntry extends LogEntry<string> {
// std/level feed the shared LogItem chrome. Alerts are not stream output,
// but they are unmistakably not stdout either, and the level is the one
// triage assigned.
super(alert.headline, alert.containerId, alert.alertId, date, "stderr", alert.headline, alertLevel(alert.level));
// Keyed on the anchor timestamp rather than the alert id: ids from Cloud
// are opaque strings now, and LogList needs a number for its v-for key.
// The anchor is unique per row here — one block per incident per window.
super(alert.headline, alert.containerId, alert.ts, date, "stderr", alert.headline, alertLevel(alert.level));
}
getComponent(): Component {
@@ -237,7 +240,7 @@ export class AlertLogEntry extends LogEntry<string> {
/**
* Stable identity for dedupe across overlapping scroll windows. Keyed on the
* ANCHOR as well as the alert: one incident legitimately marks every window
* it was active in, and keying on alertId alone would let a follow-up anchor
* it was active in, and keying on the alert alone would let a follow-up anchor
* loaded first swallow the origin loaded later.
*/
public get anchorKey(): string {
+2 -2
View File
@@ -26,7 +26,7 @@ type AlertResult struct {
// server-side to the connecting instance's (user_id, api_key_id) — Cloud
// derives those from the auth metadata, never the request body.
type AlertHit struct {
AlertID int64 `json:"alertId"`
AlertID string `json:"alertId"`
ContainerID string `json:"containerId"`
HostID string `json:"hostId"`
// LogID is Cloud's copy of Dozzle's FNV-32a hash of the line that
@@ -74,7 +74,7 @@ type EventHit struct {
Message string `json:"message,omitempty"`
Type string `json:"type,omitempty"`
Detail string `json:"detail,omitempty"`
AlertID int64 `json:"alertId,omitempty"`
AlertID string `json:"alertId,omitempty"`
// Suppressed means the event produced no notification of its own: it was
// folded into an alert already sent, or never reached one at all.
Suppressed bool `json:"suppressed"`
+1 -1
View File
@@ -46,7 +46,7 @@ func doAlerts(h *handler, query string) *httptest.ResponseRecorder {
}
func okResult(*alertCall) (*cloud.AlertResult, error) {
return &cloud.AlertResult{Hits: []cloud.AlertHit{{AlertID: 1, ContainerID: "abc"}}}, nil
return &cloud.AlertResult{Hits: []cloud.AlertHit{{AlertID: "6teOh3RH", ContainerID: "abc"}}}, nil
}
func TestCloudAlerts_UnwiredIs503(t *testing.T) {
+16 -11
View File
@@ -2513,10 +2513,13 @@ func (x *GetAlertsRequest) GetIncludeEvents() bool {
}
type AlertHit struct {
state protoimpl.MessageState `protogen:"open.v1"`
AlertId int64 `protobuf:"varint,1,opt,name=alert_id,json=alertId,proto3" json:"alert_id,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"`
HostId string `protobuf:"bytes,3,opt,name=host_id,json=hostId,proto3" json:"host_id,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
// Opaque, sqids-encoded — never the raw BIGSERIAL, which would leak total
// alert counts to anything reading a response or a deep link. Decoding it is
// Cloud's business; to Dozzle it is a string to compare and nothing more.
AlertId string `protobuf:"bytes,1,opt,name=alert_id,json=alertId,proto3" json:"alert_id,omitempty"`
ContainerId string `protobuf:"bytes,2,opt,name=container_id,json=containerId,proto3" json:"container_id,omitempty"`
HostId string `protobuf:"bytes,3,opt,name=host_id,json=hostId,proto3" json:"host_id,omitempty"`
// FNV-32a hash of the log line that triggered this alert — the same id
// Dozzle stamps on LogEvent.Id. Lets the viewer splice the alert in
// directly after its trigger line instead of guessing from a timestamp.
@@ -2593,11 +2596,11 @@ func (*AlertHit) Descriptor() ([]byte, []int) {
return file_cloud_proto_rawDescGZIP(), []int{28}
}
func (x *AlertHit) GetAlertId() int64 {
func (x *AlertHit) GetAlertId() string {
if x != nil {
return x.AlertId
}
return 0
return ""
}
func (x *AlertHit) GetContainerId() string {
@@ -2727,7 +2730,9 @@ type EventHit struct {
// 'log' | 'metric' | 'event' — the notification's own type.
Type string `protobuf:"bytes,7,opt,name=type,proto3" json:"type,omitempty"`
// The incident that absorbed this event; 0 when it never reached an alert.
AlertId int64 `protobuf:"varint,8,opt,name=alert_id,json=alertId,proto3" json:"alert_id,omitempty"`
// Opaque, sqids-encoded — see AlertHit.alert_id. Empty when the event never
// reached an alert.
AlertId string `protobuf:"bytes,8,opt,name=alert_id,json=alertId,proto3" json:"alert_id,omitempty"`
// True when this event produced no delivered notification of its own: it
// arrived after its alert had already been sent and was folded in silently,
// or it never became an alert at all.
@@ -2820,11 +2825,11 @@ func (x *EventHit) GetType() string {
return ""
}
func (x *EventHit) GetAlertId() int64 {
func (x *EventHit) GetAlertId() string {
if x != nil {
return x.AlertId
}
return 0
return ""
}
func (x *EventHit) GetSuppressed() bool {
@@ -3117,7 +3122,7 @@ const file_cloud_proto_rawDesc = "" +
"\x12include_follow_ups\x18\x06 \x01(\bR\x10includeFollowUps\x12%\n" +
"\x0einclude_events\x18\a \x01(\bR\rincludeEvents\"\xa8\x04\n" +
"\bAlertHit\x12\x19\n" +
"\balert_id\x18\x01 \x01(\x03R\aalertId\x12!\n" +
"\balert_id\x18\x01 \x01(\tR\aalertId\x12!\n" +
"\fcontainer_id\x18\x02 \x01(\tR\vcontainerId\x12\x17\n" +
"\ahost_id\x18\x03 \x01(\tR\x06hostId\x12\x15\n" +
"\x06log_id\x18\x04 \x01(\rR\x05logId\x12 \n" +
@@ -3145,7 +3150,7 @@ const file_cloud_proto_rawDesc = "" +
"\x05level\x18\x05 \x01(\tR\x05level\x12\x18\n" +
"\amessage\x18\x06 \x01(\tR\amessage\x12\x12\n" +
"\x04type\x18\a \x01(\tR\x04type\x12\x19\n" +
"\balert_id\x18\b \x01(\x03R\aalertId\x12\x1e\n" +
"\balert_id\x18\b \x01(\tR\aalertId\x12\x1e\n" +
"\n" +
"suppressed\x18\t \x01(\bR\n" +
"suppressed\x12\x16\n" +
+7 -2
View File
@@ -367,7 +367,10 @@ message GetAlertsRequest {
}
message AlertHit {
int64 alert_id = 1;
// Opaque, sqids-encoded — never the raw BIGSERIAL, which would leak total
// alert counts to anything reading a response or a deep link. Decoding it is
// Cloud's business; to Dozzle it is a string to compare and nothing more.
string alert_id = 1;
string container_id = 2;
string host_id = 3;
// FNV-32a hash of the log line that triggered this alert — the same id
@@ -428,7 +431,9 @@ message EventHit {
// 'log' | 'metric' | 'event' — the notification's own type.
string type = 7;
// The incident that absorbed this event; 0 when it never reached an alert.
int64 alert_id = 8;
// Opaque, sqids-encoded — see AlertHit.alert_id. Empty when the event never
// reached an alert.
string alert_id = 8;
// True when this event produced no delivered notification of its own: it
// arrived after its alert had already been sent and was folded in silently,
// or it never became an alert at all.