@@ -171,6 +171,10 @@ type supervisor struct {
171171}
172172
173173func newSupervisor (cfg Config , deps Deps , logger * log.Logger ) * supervisor {
174+ // Default no-op telemetry when the daemon doesn't wire one.
175+ if deps .Telemetry == nil {
176+ deps .Telemetry = noopEmitter {}
177+ }
174178 return & supervisor {
175179 cfg : cfg ,
176180 deps : deps ,
@@ -1035,36 +1039,49 @@ func (s *supervisor) CallFrom(ctx context.Context, callerID, appID, method strin
10351039// connection is dialed per-call — simple and lets the app's own
10361040// concurrency handle multiple in-flight calls. Returns the typed gate
10371041// errors above, or otherwise propagates the app's IPC response/error.
1042+ //
1043+ // After every call (success or failure) a telemetry usage event is
1044+ // emitted — best-effort, never blocks the caller.
10381045func (s * supervisor ) callFrom (ctx context.Context , callerID , appID , method string , args , out any ) error {
10391046 s .mu .RLock ()
10401047 app , ok := s .installed [appID ]
10411048 caller , callerOK := s .installed [callerID ]
10421049 ready := s .ready [appID ]
10431050 s .mu .RUnlock ()
10441051 if ! ok {
1045- return fmt .Errorf ("%w: %s" , ErrAppNotInstalled , appID )
1052+ err := fmt .Errorf ("%w: %s" , ErrAppNotInstalled , appID )
1053+ s .emitUsage (callerID , appID , method , false , 0 , err .Error ())
1054+ return err
10461055 }
10471056 // Broker-surface gate: only methods the app explicitly exposes are
10481057 // dispatchable, for every caller including the daemon itself.
10491058 if ! app .Manifest .ExposesMethod (method ) {
1050- return fmt .Errorf ("%w: %s.%s" , ErrMethodNotExposed , appID , method )
1059+ err := fmt .Errorf ("%w: %s.%s" , ErrMethodNotExposed , appID , method )
1060+ s .emitUsage (callerID , appID , method , false , 0 , err .Error ())
1061+ return err
10511062 }
10521063 // Cross-app grant gate: a calling app must have declared an ipc.call
10531064 // grant targeting the specific method it wants to invoke. Trusted
10541065 // callers (empty callerID) skip this — they are the broker itself.
10551066 if callerID != "" {
10561067 if ! callerOK {
1057- return fmt .Errorf ("%w: caller %q not installed" , ErrGrantMissing , callerID )
1068+ err := fmt .Errorf ("%w: caller %q not installed" , ErrGrantMissing , callerID )
1069+ s .emitUsage (callerID , appID , method , false , 0 , err .Error ())
1070+ return err
10581071 }
10591072 target := appID + "." + method
10601073 if ! caller .Manifest .HasGrant ("ipc.call" , target ) {
1061- return fmt .Errorf ("%w: %s -> %s" , ErrGrantMissing , callerID , target )
1074+ err := fmt .Errorf ("%w: %s -> %s" , ErrGrantMissing , callerID , target )
1075+ s .emitUsage (callerID , appID , method , false , 0 , err .Error ())
1076+ return err
10621077 }
10631078 }
10641079 if ! ready {
10651080 // Give it a brief moment in case it just spawned.
10661081 if ! s .awaitReady (ctx , appID , 1 * time .Second ) {
1067- return fmt .Errorf ("%w: %s" , ErrAppNotReady , appID )
1082+ err := fmt .Errorf ("%w: %s" , ErrAppNotReady , appID )
1083+ s .emitUsage (callerID , appID , method , false , 0 , err .Error ())
1084+ return err
10681085 }
10691086 }
10701087
@@ -1082,7 +1099,39 @@ func (s *supervisor) callFrom(ctx context.Context, callerID, appID, method strin
10821099 if dl , ok := ctx .Deadline (); ok {
10831100 _ = conn .SetDeadline (dl )
10841101 }
1085- return ipc .Call (conn , method , args , out )
1102+
1103+ start := time .Now ()
1104+ err = ipc .Call (conn , method , args , out )
1105+ dur := time .Since (start ).Milliseconds ()
1106+ if err != nil {
1107+ s .emitUsage (callerID , appID , method , false , dur , err .Error ())
1108+ } else {
1109+ s .emitUsage (callerID , appID , method , true , dur , "" )
1110+ }
1111+ return err
1112+ }
1113+
1114+ // emitUsage best-effort fires a telemetry event into the Deps.Telemetry
1115+ // emitter. It never blocks and recovers from emitter panics so a buggy
1116+ // downstream never sinks the supervisor call path.
1117+ func (s * supervisor ) emitUsage (callerID , appID , method string , ok bool , durMs int64 , errMsg string ) {
1118+ t := s .deps .Telemetry
1119+ if t == nil {
1120+ return
1121+ }
1122+ defer func () {
1123+ if r := recover (); r != nil {
1124+ s .logger .Printf ("telemetry emitter panicked: %v" , r )
1125+ }
1126+ }()
1127+ t .Emit (TelemetryEvent {
1128+ AppID : appID ,
1129+ Method : method ,
1130+ CallerID : callerID ,
1131+ OK : ok ,
1132+ DurMs : durMs ,
1133+ ErrMsg : errMsg ,
1134+ })
10861135}
10871136
10881137// awaitReady polls the ready bit for app until it flips true or the
0 commit comments