Skip to content

feat(sip): add granular call and transfer lifecycle webhook events#126

Open
nilay-automatesmb wants to merge 2 commits into
rapidaai:mainfrom
Automatesmb:feature/sip-lifecycle-webhooks
Open

feat(sip): add granular call and transfer lifecycle webhook events#126
nilay-automatesmb wants to merge 2 commits into
rapidaai:mainfrom
Automatesmb:feature/sip-lifecycle-webhooks

Conversation

@nilay-automatesmb

@nilay-automatesmb nilay-automatesmb commented May 29, 2026

Copy link
Copy Markdown

Description

Adds granular SIP call and transfer lifecycle webhook events for CRM workflows, extending the existing SIP webhook pipeline without adding websockets or a new realtime server.

Highlights:

  • Adds call lifecycle events: call.created, call.no_answer, call.busy, call.rejected (and preserves existing call.ringing, call.answered, call.cancelled, call.ended, call.failed)
  • Adds transfer lifecycle events: transfer.requested, transfer.attempt.ended, transfer.cancelled (and preserves existing transfer.attempt.started, transfer.connected, transfer.failed)
  • Defines call.media.started and transfer.target.ringing event types with wiring, but does not emit them yet without reliable low-level hooks
  • Keeps webhook delivery async through existing executor path so failures do not block SIP call setup
  • Adds/updates unit tests for event mapping and lifecycle behavior

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Configuration change
  • Refactoring (no functional changes)
  • Test update
  • Security fix

Related Issues

Fixes #125

Checklist

General

  • I have read the CONTRIBUTING guidelines
  • My code follows the project's coding standards
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas

Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested my changes on multiple platforms (if applicable)

Documentation

  • I have updated the documentation accordingly
  • I have updated the API documentation (if applicable)

Security

  • My changes do not introduce any security vulnerabilities
  • I have not committed any sensitive data (API keys, passwords, etc.)

Screenshots (if applicable)

N/A

Additional Notes

Go toolchain was not available in my execution environment, so I could not run go test locally in this session (go command not found).
I added/updated tests in:

  • api/assistant-api/sip/pipeline/signal_lifecycle_test.go
  • api/assistant-api/sip/webhook_lifecycle_test.go
  • pkg/utils/rapida_event_test.go

Changed files:

  • api/assistant-api/sip/infra/pipeline.go
  • api/assistant-api/sip/infra/server.go
  • api/assistant-api/sip/infra/server_inbound.go
  • api/assistant-api/sip/infra/types.go
  • api/assistant-api/sip/pipeline/dispatcher.go
  • api/assistant-api/sip/pipeline/signal.go
  • api/assistant-api/sip/pipeline/transfer.go
  • api/assistant-api/sip/sip.go
  • api/assistant-api/sip/webhook_lifecycle.go
  • api/assistant-api/sip/pipeline/signal_lifecycle_test.go
  • api/assistant-api/sip/webhook_lifecycle_test.go
  • pkg/utils/rapida_event.go
  • pkg/utils/rapida_event_test.go

Summary by CodeRabbit

  • New Features

    • Added call lifecycle events (created, ringing, answered, media started) and richer transfer lifecycle events (requested, attempt started, target ringing, connected, attempt ended, cancelled, failed)
    • Enabled assistant webhooks for SIP lifecycle events with conditional filtering
  • Improvements

    • Standardized lifecycle webhook payloads and more precise error/classification for call and transfer failures
  • Tests

    • Added unit tests validating lifecycle webhook emission and webhook condition handling

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 527d900d-5610-4121-b645-2c87628f41e9

📥 Commits

Reviewing files that changed from the base of the PR and between 0d08b62 and 3c4302b.

📒 Files selected for processing (3)
  • api/assistant-api/sip/pipeline/signal.go
  • api/assistant-api/sip/pipeline/transfer.go
  • api/assistant-api/sip/webhook_lifecycle.go

📝 Walkthrough

Walkthrough

This PR extends the SIP pipeline with granular call and transfer lifecycle webhook events, enabling CRM systems to track call session identity, transfer attempts, and normalized outcomes through existing webhook infrastructure.

Changes

SIP Call and Transfer Lifecycle Webhook Events

Layer / File(s) Summary
Pipeline event type contracts
api/assistant-api/sip/infra/pipeline.go, api/assistant-api/sip/infra/types.go
New exported pipeline structs model call lifecycle (created, ringing, answered, media-started) with call IDs and SIP URIs; transfer pipeline structs gain transfer IDs, routing modes, attempt/total counts, terminal-state fields; new metadata constants MetadataCallFromURI and MetadataCallToURI store inbound SIP addresses.
SIP server lifecycle callbacks
api/assistant-api/sip/infra/server.go, api/assistant-api/sip/infra/server_inbound.go
Server gains three callback hooks (onCreated, onRinging, onAnswered) with public setters; inbound INVITE handler captures From/To URIs in session metadata, invokes callbacks after session registration and state transitions, and calls notifyError on RTP failures.
Dispatcher lifecycle webhook wiring
api/assistant-api/sip/pipeline/dispatcher.go
Dispatcher wires new OnLifecycleWebhookFunc callback type; OnPipeline routes additional call/transfer pipeline types to the signal channel; dispatch expands case branches to invoke handlers for all lifecycle events.
Lifecycle webhook helpers and call handlers
api/assistant-api/sip/pipeline/signal.go
Implements SIP error parsing (parseSIPCode), failure classification (classifyCallFailure, classifyTransferAttemptFailure), and payload builders (callPayload, transferEventPayload); call handlers emit call.created/ringing/answered/media-started; handleCallFailed classifies and emits normalized failure reasons even when session is nil; transfer handlers emit structured payloads with attempt/target metadata.
Transfer per-attempt lifecycle
api/assistant-api/sip/pipeline/transfer.go
Enriched handleTransferInitiated with routing mode defaults and transfer ID generation; executeTransfer now emits transfer.attempt.started before each dial, transfer.attempt.ended with classified state/reason on dial/bridge outcome, transfer.cancelled conditionally for remaining targets after successful connection, and enhanced categorizeTransferError with more granular SIP code/keyword matching.
SIP engine integration
api/assistant-api/sip/sip.go
Initializes assistantHTTPLogService; registers onCreated/onRinging/onAnswered handlers in Connect; configures dispatcher with publishSIPLifecycleWebhook callback; updates assistant retrieval in assistantMiddleware and pipelineCallSetup to inject webhook metadata.
Webhook lifecycle publishing and event types
api/assistant-api/sip/webhook_lifecycle.go, pkg/utils/rapida_event.go
Implements sipWebhookCallback for HTTP lifecycle logging via assistantHTTPLogService; publishSIPLifecycleWebhook lazily loads assistant webhooks, builds envelope (ID/type/timestamp/payload), filters by event subscription and webhook.condition, executes matching webhooks; allowSIPWebhookCondition parses and evaluates direction rules; adds call/transfer lifecycle event constants (call.created/ringing/answered/no-answer/busy/rejected/cancelled/ended/failed, transfer.requested/attempt.started/target.ringing/connected/attempt.ended/cancelled/failed).
Tests
api/assistant-api/sip/pipeline/signal_lifecycle_test.go, api/assistant-api/sip/webhook_lifecycle_test.go, pkg/utils/rapida_event_test.go
New tests validate lifecycle webhook emission for call ringing/created, transfer attempt/request/ended/cancelled with payload field assertions; call failure classification with table-driven SIP error/code mapping; webhook condition evaluation (no condition allows, valid direction rules filter, invalid JSON blocks); webhook event constant string mappings.

Sequence Diagram(s)

The changes do not warrant a sequence diagram. The lifecycle webhook feature is integrated through existing infrastructure (dispatcher → webhook executor → assistant webhooks) without introducing new multi-component interaction flows; webhook emission is a series of handler invocations that directly call standardized payload builders and the lifecycle webhook callback.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes


🐰 Call and transfer events now flow,
Per-attempt and state, all aglow!
CRM systems see fine-grained light,
Transfer outcomes, ringing so bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: adding granular call and transfer lifecycle webhook events to the SIP module.
Linked Issues check ✅ Passed The PR implements all primary objectives from #125: call lifecycle events (created/ringing/answered/no_answer/busy/rejected/cancelled/ended/failed), transfer lifecycle events (requested/attempt.started/connected/attempt.ended/cancelled/failed), and proper failure classification with normalized reasons.
Out of Scope Changes check ✅ Passed All changes directly support the stated objectives. Pipeline structs, lifecycle callbacks, webhook emission logic, event constants, and tests are all necessary for the feature. No unrelated modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
api/assistant-api/sip/pipeline/transfer.go (2)

217-246: ⚡ Quick win

Track the connected target by loop index to handle duplicate targets correctly.

indexOfTarget returns the first occurrence. If a target value repeats, the connected Attempt and the cancellation skip boundary (i < connectedIndex) are computed against the wrong index — reporting a wrong attempt number and emitting spurious TransferCancelled events for earlier targets that already failed. Reuse the index from the dial loop; this also removes the redundant rescans and makes indexOfTarget (Lines 296-303) dead code.

♻️ Use the loop index instead of rescanning
 	var outboundSession *sip_infra.Session
 	var connectedTarget string
+	connectedIndex := -1
 	for i, target := range targets {
 		if err == nil {
 			outboundSession = session
 			connectedTarget = target
+			connectedIndex = i
 			d.OnPipeline(ctx, sip_infra.TransferAttemptEndedPipeline{
 		TargetURI:       connectedTarget,
-		Attempt:         indexOfTarget(targets, connectedTarget) + 1,
+		Attempt:         connectedIndex + 1,
 		TotalAttempts:   len(targets),
 		TransferID:      v.TransferID,
 		RoutingMode:     v.RoutingMode,
 	})
-	connectedIndex := indexOfTarget(targets, connectedTarget)
 	for i, target := range targets {
-		if target == connectedTarget {
+		if i == connectedIndex {
 			continue
 		}

Then drop the now-unused indexOfTarget helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/assistant-api/sip/pipeline/transfer.go` around lines 217 - 246, The loop
currently rescans targets with indexOfTarget(…) to compute connectedIndex and
Attempt, which misattributes attempt numbers when targets repeat; change the
logic in the dial loop to capture the connected target's loop index (i) when you
issue the TransferConnectedPipeline and use that i for Attempt and for the
cancellation boundary instead of calling indexOfTarget; update the
TransferConnectedPipeline.Attempt to use the loop index+1, use that same index
to skip earlier cancellations (preserve the v.RoutingMode check), and then
remove the now-unused indexOfTarget helper and its calls so duplicate target
values are handled correctly and no spurious TransferCancelledPipeline events
are emitted.

189-196: ⚡ Quick win

Preserve the last dial error in the exhausted-targets failure.

The synthesized message drops the actual SIP failure from the final attempt, so the TransferFailedPipeline.Error carries no diagnostic detail for downstream consumers/logs. Capture the last attempt error and wrap it.

♻️ Preserve and wrap the last attempt error
 	var outboundSession *sip_infra.Session
 	var connectedTarget string
+	var lastErr error
 	for i, target := range targets {
 		d.logger.Warnw("Pipeline: transfer_target_failed",
 			"call_id", v.ID, "target", target,
 			"attempt", attempt, "error", err)
+		lastErr = err
-			Error:       fmt.Errorf("all %d transfer targets failed", len(targets)),
+			Error:       fmt.Errorf("all %d transfer targets failed: %w", len(targets), lastErr),

If targets can be empty here, guard the wrap so a nil lastErr isn't formatted with %w.

As per coding guidelines, "Return errors with context wrapping using fmt.Errorf with %w verb for error chain preservation".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/assistant-api/sip/pipeline/transfer.go` around lines 189 - 196, The
TransferFailedPipeline currently emits a generic error; change it to preserve
and wrap the last dial error from the final attempt: obtain the lastErr (the
error returned by the last target dial attempt), and when building
TransferFailedPipeline.Error use fmt.Errorf("all %d transfer targets failed:
%w", len(targets), lastErr) to wrap it so the error chain is preserved; if
targets is empty or lastErr is nil, fall back to the existing fmt.Errorf("all %d
transfer targets failed", len(targets)) to avoid formatting a nil error. Ensure
this change is applied where OnPipeline is called for TransferFailedPipeline
(using v, targets, lastErr).
api/assistant-api/sip/pipeline/signal.go (1)

222-222: 💤 Low value

Consider extracting extension length limit as a named constant.

The magic number 6 (maximum extension length) would be clearer as a named constant, e.g., maxExtensionLength = 6.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/assistant-api/sip/pipeline/signal.go` at line 222, Extract the magic
number 6 used in the extension-length check into a named constant (e.g.,
maxExtensionLength = 6) and replace the literal in the condition "if len(user) >
0 && len(user) <= 6" with that constant; define the constant near other
file-level constants (or at top of api/assistant-api/sip/pipeline/signal.go) and
add a short comment indicating it represents the maximum extension length so
callers reading the "len(user) <= maxExtensionLength" check understand its
meaning.
api/assistant-api/sip/webhook_lifecycle.go (1)

15-24: ⚡ Quick win

Import group is not goimports-sorted; likely fails golangci-lint.

Within the github.com/rapidaai group, goimports orders by import path. internal_services (.../internal/services) is placed after internal_webhook (.../internal/webhook), so the block is not sorted and the lint check will fail.

♻️ Proposed reorder
 	internal_condition "github.com/rapidaai/api/assistant-api/internal/condition"
 	internal_assistant_entity "github.com/rapidaai/api/assistant-api/internal/entity/assistants"
+	internal_services "github.com/rapidaai/api/assistant-api/internal/services"
 	internal_type "github.com/rapidaai/api/assistant-api/internal/type"
 	internal_webhook "github.com/rapidaai/api/assistant-api/internal/webhook"
-	internal_services "github.com/rapidaai/api/assistant-api/internal/services"
 	sip_infra "github.com/rapidaai/api/assistant-api/sip/infra"

As per coding guidelines: "Go import groups must follow order: stdlib, external, then github.com/rapidaai (enforced by goimports in golangci-lint)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/assistant-api/sip/webhook_lifecycle.go` around lines 15 - 24, Reorder the
imports in the import block so the github.com/rapidaai group is sorted by path
(e.g., place internal_services (.../internal/services) before internal_webhook
(.../internal/webhook)); specifically adjust the import block containing
internal_condition, internal_assistant_entity, internal_type, internal_webhook,
internal_services, sip_infra, gorm_generator, types, type_enums, utils to be
goimports-sorted (alphabetical within the rapidaai group) and then run
goimports/golangci-lint to verify.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/assistant-api/sip/webhook_lifecycle.go`:
- Around line 59-61: The CreateLog call currently returns its error directly,
losing context; update the return to wrap the error with fmt.Errorf using %w
(e.g. return fmt.Errorf("persisting SIP HTTP log in CreateLog: %w", err)) so the
call site and cause are preserved, and add/import fmt if not already imported;
ensure this change is applied at the return after the CreateLog invocation in
webhook_lifecycle.go (the block that currently does "if err != nil { return err
}").

---

Nitpick comments:
In `@api/assistant-api/sip/pipeline/signal.go`:
- Line 222: Extract the magic number 6 used in the extension-length check into a
named constant (e.g., maxExtensionLength = 6) and replace the literal in the
condition "if len(user) > 0 && len(user) <= 6" with that constant; define the
constant near other file-level constants (or at top of
api/assistant-api/sip/pipeline/signal.go) and add a short comment indicating it
represents the maximum extension length so callers reading the "len(user) <=
maxExtensionLength" check understand its meaning.

In `@api/assistant-api/sip/pipeline/transfer.go`:
- Around line 217-246: The loop currently rescans targets with indexOfTarget(…)
to compute connectedIndex and Attempt, which misattributes attempt numbers when
targets repeat; change the logic in the dial loop to capture the connected
target's loop index (i) when you issue the TransferConnectedPipeline and use
that i for Attempt and for the cancellation boundary instead of calling
indexOfTarget; update the TransferConnectedPipeline.Attempt to use the loop
index+1, use that same index to skip earlier cancellations (preserve the
v.RoutingMode check), and then remove the now-unused indexOfTarget helper and
its calls so duplicate target values are handled correctly and no spurious
TransferCancelledPipeline events are emitted.
- Around line 189-196: The TransferFailedPipeline currently emits a generic
error; change it to preserve and wrap the last dial error from the final
attempt: obtain the lastErr (the error returned by the last target dial
attempt), and when building TransferFailedPipeline.Error use fmt.Errorf("all %d
transfer targets failed: %w", len(targets), lastErr) to wrap it so the error
chain is preserved; if targets is empty or lastErr is nil, fall back to the
existing fmt.Errorf("all %d transfer targets failed", len(targets)) to avoid
formatting a nil error. Ensure this change is applied where OnPipeline is called
for TransferFailedPipeline (using v, targets, lastErr).

In `@api/assistant-api/sip/webhook_lifecycle.go`:
- Around line 15-24: Reorder the imports in the import block so the
github.com/rapidaai group is sorted by path (e.g., place internal_services
(.../internal/services) before internal_webhook (.../internal/webhook));
specifically adjust the import block containing internal_condition,
internal_assistant_entity, internal_type, internal_webhook, internal_services,
sip_infra, gorm_generator, types, type_enums, utils to be goimports-sorted
(alphabetical within the rapidaai group) and then run goimports/golangci-lint to
verify.
🪄 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: 7df713d5-ebcb-4da4-8e12-2b1315dd3de6

📥 Commits

Reviewing files that changed from the base of the PR and between 2a946d8 and 0d08b62.

📒 Files selected for processing (13)
  • api/assistant-api/sip/infra/pipeline.go
  • api/assistant-api/sip/infra/server.go
  • api/assistant-api/sip/infra/server_inbound.go
  • api/assistant-api/sip/infra/types.go
  • api/assistant-api/sip/pipeline/dispatcher.go
  • api/assistant-api/sip/pipeline/signal.go
  • api/assistant-api/sip/pipeline/signal_lifecycle_test.go
  • api/assistant-api/sip/pipeline/transfer.go
  • api/assistant-api/sip/sip.go
  • api/assistant-api/sip/webhook_lifecycle.go
  • api/assistant-api/sip/webhook_lifecycle_test.go
  • pkg/utils/rapida_event.go
  • pkg/utils/rapida_event_test.go

Comment thread api/assistant-api/sip/webhook_lifecycle.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.

[Feature]: Add granular SIP call + transfer lifecycle webhook events for CRM workflows

1 participant