fix(service/issue): route analytics.IssueCreated through obsmetrics.RecordEvent (MUL-2671)

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 <github@multica.ai>
This commit is contained in:
J
2026-06-03 17:46:22 +08:00
parent 0bbcd33096
commit aaf0d7b2fe
2 changed files with 12 additions and 5 deletions

View File

@@ -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

View File

@@ -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),