From aaf0d7b2feeef609a576ffefd6b676ae9c722c57 Mon Sep 17 00:00:00 2001 From: J Date: Wed, 3 Jun 2026 17:46:22 +0800 Subject: [PATCH] fix(service/issue): route analytics.IssueCreated through obsmetrics.RecordEvent (MUL-2671) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's TestNoNakedAnalyticsCaptureInHandlersOrServices guard caught the post-merge analytics call in IssueService.captureCreatedAnalytics that still used s.Analytics.Capture(...) directly. Main added that lint to prevent the Prometheus and PostHog sides from drifting — any new analytics.* event must go through obsmetrics.RecordEvent so the business-metrics collector and the PostHog client fire from the same call site. Fix mirrors how TaskService handles it: IssueService gains a Metrics *obsmetrics.BusinessMetrics field (router wires it via h.IssueService.Metrics = opts.BusinessMetrics next to the existing TaskService line), and the in-service Capture call becomes obsmetrics.RecordEvent(s.Analytics, s.Metrics, ...). nil-safe by construction — RecordEvent treats a nil Metrics as PostHog-only. Co-authored-by: multica-agent --- server/cmd/server/router.go | 1 + server/internal/service/issue.go | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/server/cmd/server/router.go b/server/cmd/server/router.go index fcfcdf52d..147db5049 100644 --- a/server/cmd/server/router.go +++ b/server/cmd/server/router.go @@ -155,6 +155,7 @@ func NewRouterWithOptions(pool *pgxpool.Pool, hub *realtime.Hub, bus *events.Bus h := handler.New(queries, pool, hub, bus, emailSvc, store, cfSigner, analyticsClient, signupConfig, daemonHub) h.Metrics = opts.BusinessMetrics h.TaskService.Metrics = opts.BusinessMetrics + h.IssueService.Metrics = opts.BusinessMetrics if opts.BusinessMetrics != nil { // Wire the BusinessMetrics receiver into the cloud runtime client // so every outbound Fleet/Gateway request feeds the diff --git a/server/internal/service/issue.go b/server/internal/service/issue.go index ba9c50fca..a18a0badc 100644 --- a/server/internal/service/issue.go +++ b/server/internal/service/issue.go @@ -11,6 +11,7 @@ import ( "github.com/multica-ai/multica/server/internal/events" "github.com/multica-ai/multica/server/internal/issueguard" "github.com/multica-ai/multica/server/internal/issueposition" + obsmetrics "github.com/multica-ai/multica/server/internal/metrics" "github.com/multica-ai/multica/server/internal/util" db "github.com/multica-ai/multica/server/pkg/db/generated" "github.com/multica-ai/multica/server/pkg/protocol" @@ -23,10 +24,15 @@ import ( // service deliberately does NOT depend on http.Request — callers parse // their own transport and pass a fully-resolved IssueCreateParams. type IssueService struct { - Queries *db.Queries - TxStarter TxStarter - Bus *events.Bus - Analytics analytics.Client + Queries *db.Queries + TxStarter TxStarter + Bus *events.Bus + Analytics analytics.Client + // Metrics is the shared business-metrics collector. Wired by + // cmd/server/router.go after construction; nil in tests / self-hosted + // without the metrics listener — obsmetrics.RecordEvent treats a nil + // Metrics as "PostHog only", so leaving it unset is safe. + Metrics *obsmetrics.BusinessMetrics TaskService *TaskService } @@ -338,7 +344,7 @@ func (s *IssueService) captureCreatedAnalytics(issue db.Issue, creatorType, acto if creatorType == "agent" { analyticsActorID = "agent:" + actorID } - s.Analytics.Capture(analytics.IssueCreated( + obsmetrics.RecordEvent(s.Analytics, s.Metrics, analytics.IssueCreated( analyticsActorID, util.UUIDToString(issue.WorkspaceID), util.UUIDToString(issue.ID),