Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion reader/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def _set_user_experiments(user, value):
profile.save()

if changed:
dispatch_chatbot_opt_in_webhook(user.email, experiments_enabled)
interface_language = profile.settings.get("interface_language", "english")
dispatch_chatbot_opt_in_webhook(user.email, experiments_enabled, interface_language)


if not hasattr(User, "experiments"):
Expand Down
6 changes: 3 additions & 3 deletions reader/tests/experiments_admin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,14 @@ def test_set_user_experiments_fires_webhook_on_change(self, mock_dispatch):
mock_dispatch.reset_mock()
_set_user_experiments(self.user, True) # first call — created, fires
self.assertEqual(mock_dispatch.call_count, 1)
mock_dispatch.assert_called_with(self.user.email, True)
mock_dispatch.assert_called_with(self.user.email, True, "english")

mock_dispatch.reset_mock()
_set_user_experiments(self.user, True) # same value — no fire
mock_dispatch.assert_not_called()

_set_user_experiments(self.user, False) # changed — fires
mock_dispatch.assert_called_once_with(self.user.email, False)
mock_dispatch.assert_called_once_with(self.user.email, False, "english")

# Keep compatibility with older test node IDs.
def test_user_experiment_settings_admin_updates_profile_without_duplicates(self, _mock_dispatch):
Expand Down Expand Up @@ -225,7 +225,7 @@ def test_csv_upload_fires_webhook_for_each_user(self, mock_dispatch):

self.assertEqual(mock_dispatch.call_count, len(emails))
for u in self.existing_users:
mock_dispatch.assert_any_call(u.email, True)
mock_dispatch.assert_any_call(u.email, True, "english")

def test_case_insensitive_email_matching(self, _mock_dispatch):
user = self.existing_users[0]
Expand Down
24 changes: 13 additions & 11 deletions sefaria/helper/crm/chatbot_webhook_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,14 @@ def test_extract_error_detail_generic_exception():
def test_webhook_success(mock_post):
mock_post.return_value = make_mock_response(200, json_body={"success": True})

result = send_chatbot_opt_in_webhook.apply(args=["user@example.com", True])
result = send_chatbot_opt_in_webhook.apply(args=["user@example.com", True, "english"])

assert result.successful()
mock_post.assert_called_once()
payload = mock_post.call_args.kwargs["json"]
assert payload["data"]["email"] == "user@example.com"
assert payload["data"]["optIn"] is True
assert payload["data"]["interfaceLanguage"] == "english"
assert "id" in payload


Expand All @@ -80,7 +81,7 @@ def test_webhook_success_false_triggers_retry(mock_post):
200, json_body={"success": False, "error": "invalid email"}
)

send_chatbot_opt_in_webhook.apply(args=["user@example.com", True])
send_chatbot_opt_in_webhook.apply(args=["user@example.com", True, "english"])

# initial attempt + 1 retry = 2 calls
assert mock_post.call_count == 2
Expand All @@ -91,7 +92,7 @@ def test_webhook_success_false_triggers_retry(mock_post):
def test_webhook_final_failure_reports_to_sentry(mock_post, mock_capture):
mock_post.side_effect = ConnectionError("connection refused")

send_chatbot_opt_in_webhook.apply(args=["user@example.com", True])
send_chatbot_opt_in_webhook.apply(args=["user@example.com", True, "english"])

assert mock_post.call_count == 2
mock_capture.assert_called_once()
Expand All @@ -104,7 +105,7 @@ def test_webhook_http_500_flow_exception(mock_post, mock_capture):
resp.raise_for_status.side_effect = requests.HTTPError(response=resp)
mock_post.return_value = resp

send_chatbot_opt_in_webhook.apply(args=["user@example.com", True])
send_chatbot_opt_in_webhook.apply(args=["user@example.com", True, "english"])

assert mock_post.call_count == 2
mock_capture.assert_called_once()
Expand All @@ -118,7 +119,7 @@ def test_webhook_first_fail_second_succeeds(mock_post):

mock_post.side_effect = [fail_resp, ok_resp]

result = send_chatbot_opt_in_webhook.apply(args=["user@example.com", True])
result = send_chatbot_opt_in_webhook.apply(args=["user@example.com", True, "english"])

assert mock_post.call_count == 2
assert result.successful()
Expand All @@ -128,10 +129,11 @@ def test_webhook_first_fail_second_succeeds(mock_post):
def test_webhook_opt_out_sends_false(mock_post):
mock_post.return_value = make_mock_response(200, json_body={"success": True})

send_chatbot_opt_in_webhook.apply(args=["user@example.com", False])
send_chatbot_opt_in_webhook.apply(args=["user@example.com", False, "hebrew"])

payload = mock_post.call_args.kwargs["json"]
assert payload["data"]["optIn"] is False
assert payload["data"]["interfaceLanguage"] == "hebrew"


@patch("sefaria.helper.crm.tasks.sentry_sdk.capture_exception")
Expand All @@ -144,7 +146,7 @@ def test_webhook_rejects_get_with_400(mock_post, mock_capture):
resp.raise_for_status.side_effect = requests.HTTPError(response=resp)
mock_post.return_value = resp

send_chatbot_opt_in_webhook.apply(args=["user@example.com", True])
send_chatbot_opt_in_webhook.apply(args=["user@example.com", True, "english"])

assert mock_post.call_count == 2
mock_capture.assert_called_once()
Expand All @@ -159,7 +161,7 @@ def test_dispatch_celery_enabled_uses_apply_async(mock_task):
from sefaria.helper.crm.tasks import dispatch_chatbot_opt_in_webhook

with patch("sefaria.helper.crm.tasks.CELERY_ENABLED", True):
dispatch_chatbot_opt_in_webhook("user@example.com", True)
dispatch_chatbot_opt_in_webhook("user@example.com", True, "english")

mock_task.apply_async.assert_called_once()

Expand All @@ -171,7 +173,7 @@ def test_dispatch_celery_disabled_calls_synchronously(mock_post):
mock_post.return_value = make_mock_response(200, json_body={"success": True})

with patch("sefaria.helper.crm.tasks.CELERY_ENABLED", False):
dispatch_chatbot_opt_in_webhook("user@example.com", True)
dispatch_chatbot_opt_in_webhook("user@example.com", True, "hebrew")

mock_post.assert_called_once()

Expand Down Expand Up @@ -235,7 +237,7 @@ def test_opt_in_fires_webhook(self, mock_dispatch, client, test_user):
response = client.post("/api/profile/experiments/opt-in")

assert response.status_code == 200
mock_dispatch.assert_called_once_with(test_user.email, True)
mock_dispatch.assert_called_once_with(test_user.email, True, "english")

@mock.patch("reader.models.dispatch_chatbot_opt_in_webhook")
def test_repeated_opt_in_does_not_refire(self, mock_dispatch, client, test_user):
Expand Down Expand Up @@ -269,7 +271,7 @@ def test_toggle_off_fires_webhook(self, mock_dispatch, client, test_user_with_pr
)

assert response.status_code == 200
mock_dispatch.assert_called_once_with(user.email, False)
mock_dispatch.assert_called_once_with(user.email, False, "english")

@mock.patch("reader.models.dispatch_chatbot_opt_in_webhook")
def test_same_value_does_not_fire_webhook(self, mock_dispatch, client, test_user_with_profile):
Expand Down
11 changes: 6 additions & 5 deletions sefaria/helper/crm/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
logger = structlog.get_logger(__name__)

CHATBOT_OPT_IN_WEBHOOK_URL = (
"https://sefariainc.my.salesforce-sites.com/services/apexrest/Streams/webhookflow"
"https://sefariainc--stage.sandbox.my.salesforce-sites.com/services/apexrest/Streams/webhookflow"
)


Expand All @@ -23,7 +23,7 @@
acks_late=True,
ignore_result=True,
)
def send_chatbot_opt_in_webhook(self: Any, email: str, opt_in: bool) -> None:
def send_chatbot_opt_in_webhook(self: Any, email: str, opt_in: bool, interface_language: str = "english") -> None:
"""
POST a chatbot experiment opt-in/opt-out event to the Salesforce webhook.

Expand All @@ -35,6 +35,7 @@ def send_chatbot_opt_in_webhook(self: Any, email: str, opt_in: bool) -> None:
"data": {
"email": email,
"optIn": opt_in,
"interfaceLanguage": interface_language,
},
}

Expand Down Expand Up @@ -91,17 +92,17 @@ def extract_error_detail(exc: Exception) -> str:
return str(exc)


def dispatch_chatbot_opt_in_webhook(email: str, opt_in: bool) -> None:
def dispatch_chatbot_opt_in_webhook(email: str, opt_in: bool, interface_language: str = "english") -> None:
"""Fire the Salesforce webhook for a chatbot experiment opt-in/opt-out change."""
if not email:
return
if CELERY_ENABLED:
send_chatbot_opt_in_webhook.apply_async(
args=[email, opt_in],
args=[email, opt_in, interface_language],
queue=CeleryQueue.TASKS.value,
)
else:
try:
send_chatbot_opt_in_webhook(email, opt_in)
send_chatbot_opt_in_webhook(email, opt_in, interface_language)
except Exception:
logger.warning("chatbot_opt_in_webhook_sync_failed", email=email, exc_info=True)
Loading