Spaces:
Running
Running
| """Analytics โ in-your-control metrics from implementation_plan/06 ยง5.""" | |
| from tests._helpers import ( | |
| auth_headers, | |
| create_creator, | |
| create_persona, | |
| enable_widget, | |
| seed_approved_clip, | |
| signup, | |
| ) | |
| def _start_and_send(client, persona, text: str) -> str: | |
| r = client.post("/widget/sessions", json={"public_key": persona["public_key"]}) | |
| sid = r.json()["id"] | |
| client.post(f"/widget/sessions/{sid}/message", json={"text": text}) | |
| return sid | |
| def test_analytics_basic_counts(client, fake_storage, no_redis): | |
| token = signup(client, "an1@example.com") | |
| creator = create_creator(client, token) | |
| persona = create_persona(client, token, creator["id"]) | |
| enable_widget(client, token, persona["id"]) | |
| seed_approved_clip(client, token, persona["id"], intent_tag="pricing", | |
| transcript="ืืืืืจ X") | |
| # 2 sessions: one served (pricing), one fallback (oos) | |
| _start_and_send(client, persona, "ืืื ืื ืขืืื?") | |
| _start_and_send(client, persona, "ืื ืืืื ืืช ืืืจืืงื") | |
| r = client.get( | |
| f"/personas/{persona['id']}/analytics", headers=auth_headers(token) | |
| ) | |
| assert r.status_code == 200 | |
| body = r.json() | |
| assert body["persona_id"] == persona["id"] | |
| assert body["sessions_started"] == 2 | |
| assert body["sessions_completed"] == 2 | |
| assert body["avg_questions_per_session"] == 1.0 | |
| # 1 served + 1 fallback โ fallback_rate = 0.5 | |
| assert body["fallback_rate"] == 0.5 | |
| # Reasons present (out_of_scope from the second message) | |
| assert "out_of_scope" in body["fallback_rate_by_reason"] | |
| # missing_topics โ only pricing is covered, the rest of core_intents are missing | |
| assert "who_you_are" in body["missing_topics"] | |
| assert "pricing" not in body["missing_topics"] | |
| def test_analytics_lead_conversion(client, fake_storage, no_redis): | |
| token = signup(client, "an2@example.com") | |
| creator = create_creator(client, token) | |
| persona = create_persona(client, token, creator["id"]) | |
| enable_widget(client, token, persona["id"]) | |
| # One session, one lead captured at the end | |
| sid = _start_and_send(client, persona, "ืื ืืืื ืืช ืืืจืืงื") | |
| client.post( | |
| f"/widget/sessions/{sid}/lead", | |
| json={"name": "X", "contact": "x@x.io", "question_context": "ctx"}, | |
| ) | |
| r = client.get( | |
| f"/personas/{persona['id']}/analytics", headers=auth_headers(token) | |
| ) | |
| body = r.json() | |
| assert body["leads_count"] == 1 | |
| assert body["sessions_completed"] == 1 | |
| assert body["lead_conversion_rate"] == 1.0 | |
| def test_analytics_cross_tenant_403(client, fake_storage, no_redis): | |
| token_a = signup(client, "alice-a@example.com", org="AnA") | |
| token_b = signup(client, "bob-a@example.com", org="AnB") | |
| creator = create_creator(client, token_a) | |
| persona = create_persona(client, token_a, creator["id"]) | |
| r = client.get( | |
| f"/personas/{persona['id']}/analytics", headers=auth_headers(token_b) | |
| ) | |
| assert r.status_code == 403 | |
| def test_analytics_top_intents_orders_by_count(client, fake_storage, no_redis): | |
| token = signup(client, "an3@example.com") | |
| creator = create_creator(client, token) | |
| persona = create_persona(client, token, creator["id"]) | |
| enable_widget(client, token, persona["id"]) | |
| seed_approved_clip(client, token, persona["id"], intent_tag="pricing", | |
| transcript="A") | |
| seed_approved_clip(client, token, persona["id"], intent_tag="how_it_works", | |
| transcript="B", filename="b.mp4") | |
| # 3 pricing questions, 1 how_it_works | |
| for _ in range(3): | |
| _start_and_send(client, persona, "ืืื ืื ืขืืื?") | |
| _start_and_send(client, persona, "ืืื ืืชืืืื ืขืืื?") | |
| r = client.get( | |
| f"/personas/{persona['id']}/analytics", headers=auth_headers(token) | |
| ) | |
| top = r.json()["top_intents"] | |
| assert top[0]["intent"] == "pricing" | |
| assert top[0]["count"] == 3 | |
| intents = {row["intent"]: row["count"] for row in top} | |
| assert intents.get("how_it_works") == 1 | |