MCP Analytics (מדידת השימוש בכלי ה-MCP)
- summary:
מסך אדמין שמציג את נתוני השימוש בכלי ה-MCP מתוך PostHog — בריאות הכלים, עלות הניווט בריפו, ויכולות שסוכנים ביקשו — עם מצבי הכשל ומשתני הסביבה שהוא דורש.
מה זה
שרת ה-MCP מדווח ל-PostHog אירוע על כל קריאת כלי (מדידת שימוש (PostHog MCP Analytics)). המסך
/admin/mcp קורא את הנתונים האלה חזרה ומציג אותם בוובאפ, כדי שלא יידרש
להיכנס ל-PostHog בשביל לענות על שלוש שאלות:
באילו כלים באמת משתמשים, כמה מהקריאות נכשלות — ובאיזו הודעה.
כמה קריאות עולה לסוכן לנווט בריפו, ומה הסוכן ניסה להשיג באותו סשן.
אילו יכולות סוכנים ביקשו ולא קיבלו.
הטבלה של בריאות הכלים אומרת ”20% שגיאות“. מתחתיה, באותו טאב, יושבות הקריאות שנכשלו עצמן — כלי, זמן, לקוח וההודעה. בלעדיהן העמוד הוא דוח מסירה בלי פתק המסירה: מספר כשלים בלי שם השדה שנדחה.
דרך ה-UI: דף Settings ← כלי אדמין ← MCP Analytics. ישירות:
GET /admin/mcp. הדף זמין רק לאדמינים (@admin_required).
מאיפה הנתונים מגיעים
לא משאילתה שכתובה אצלנו. הקוד קורא ל-endpoints שמורים ב-PostHog — שאילתות שחיות שם ולא בריפו:
אנדפוינט |
מה מחזיר |
טווח |
|---|---|---|
|
שורה לכל כלי: קריאות, שגיאות, אחוז, p50, p95, סשנים, נראה לאחרונה |
30 יום |
|
שורה לכל קריאה שנכשלה: זמן, כלי, לקוח, סוג השגיאה וההודעה עצמה |
30 יום |
|
שורה לכל סשן: קריאות, חיפושים, אאוטליין, קריאת תוכן, שגיאות, זמן כולל, וה**כוונה** שהסוכן כתב |
30 יום |
|
שורה לכל דיווח מהכלי הווירטואלי |
90 יום |
הערה
ck_mcp_navigation_cost (הגרסה הראשונה, עם עמודת file_reads המאוחדת)
נשאר חי ב-PostHog ואינו נמחק — הוא הבסיס להשוואה מול הטור המפוצל.
העמוד פשוט אינו קורא לו יותר. שם האנדפוינט הוא קבוע בקוד, ולכן החלפת
הגרסה היא הדבר היחיד בעמוד הזה שכן דורש דיפלוי.
ההפרדה הזו היא העיקר: שינוי שאילתה אינו דורש דיפלוי. מי שרוצה לשנות טווח, לסנן, או להוסיף עמודה — עושה זאת ב-PostHog, והעמוד מציג את מה שחזר. הקוד אינו מכיר את ה-SQL, ולכן גם אינו יכול להיסחף ממנו.
הטווח של ck_mcp_missing_capabilities ארוך יותר בכוונה: דיווח על כלי חסר הוא
אירוע נדיר, וחלון של 30 יום היה מוחק אותו לפני שמישהו הספיק לקרוא.
הערה
הטבלה של ”כלים חסרים“ ריקה עד שסוכן ידווח, וזה מצב תקין. האיסוף פעיל —
report_missing=True ב-mcp_server/analytics.py מוסיף את הכלי הווירטואלי
get_more_tools — אבל הצינור הפתוח אינו אומר שיש מה להציג. העמוד מבחין בין
השניים במפורש: ”אף סוכן עוד לא דיווח“ הוא הצלחה עם אפס שורות, ולא כשל.
ההבחנה הזו נבדקת ב-tests/test_admin_mcp_page.py, כי בלעדיה תקלה תיראה
בדיוק כמו שקט.
תקרת שורות ומאיפה מגיעה הספירה
ck_mcp_navigation_cost_v2 מחזיר שורה לכל סשן, ובלי הגבלה הוא מחזיר את כולן —
כמות שגדלה בלי תקרה. הקוד מבקש limit (בגוף הבקשה, ראו למטה), ולכן מוצג רק
החלק העליון של הרשימה, שכבר ממוינת ORDER BY started DESC.
הספירה המלאה אינה מגיעה מהמונה של ה-API: אין בתשובה שדה כזה. היא נגזרת
בשאילתה עצמה ב-count() OVER () AS total_sessions, ולכן כל שורה נושאת את
הספירה של כל הסשנים ולא של מה שהוחזר. כך שורת החיווי אומרת ”מוצגים N מתוך M“
בלי לשלם על העברת כל השורות.
ל-ck_mcp_tool_failures יש תקרה משלו, נמוכה יותר: הוא יושב בתוך טאב קיים
ולא לבד. אין לו עמודת ספירה כוללת, ולכן מה שאומר שהרשימה נחתכה הוא hasMore
בתשובה — והעמוד מציג ”יש עוד“ לפיו.
חשוב
התקציב העליון לא זז כשנוסף האנדפוינט הרביעי, וזה לא פספוס. הוא נגזר
מהמקרה הגרוע של קריאה בודדת (connect + read), וקריאה נוספת שרצה
לצידה אינה מאריכה אותו. ה-pool מקבל worker לכל אנדפוינט, ולכן הרביעי אינו
ממתין בתור. הרשימה עצמה יושבת ב-DASHBOARD_ENDPOINTS, וגם גודל ה-pool
וגם הבדיקות נגזרים ממנה — כדי שהוספת אנדפוינט חמישי לא תשבור את הבטחת
המקבול בשקט.
סימון ”טופל“ / ”נדחה“ בכלים חסרים
לכל שורה בטאב ”כלים חסרים“ יש שני כפתורים: ✓ לסימון שהדיווח טופל, ו-✗ לסימון שנדחה. לחיצה על כפתור שכבר פעיל מבטלת את הסימון וחוזרת ל“לא טופל“, ושני הכפתורים דוחקים זה את זה — דיווח אינו יכול להיות גם טופל וגם נדחה.
הסימון חי בדפדפן, לא בשרת. השורות מגיעות מ-PostHog ואי אפשר לכתוב אליהן, ולכן מה שנשמר הוא שכבה דקה מעליהן ב-localStorage תחת המפתח mcpMissingMarks — בדיוק כמו באנר ”שגיאות חדשות“ באותו עמוד. המשמעות המעשית: הסימון הוא של הדפדפן הזה בלבד. במכשיר אחר, או אחרי ניקוי נתוני אתר, הוא לא יהיה שם.
הזהות של שורה לצורך הסימון היא <זמן הדיווח>|<סשן>, מה-data-cap-key שנכתב על <tr>. שני הערכים אינם משתנים בין טעינות, ולכן הסימון נשאר על אותו דיווח גם כשמצטרפים דיווחים חדשים מעליו. שורה בלי חותמת זמן אינה ניתנת לסימון ומציגה מקף: שתי שורות כאלה היו מקבלות מפתח זהה ונסמנות יחד.
חשוב
מה שמצויר הוא מה שנקרא חזרה מהאחסון, ולא מה שנלחץ. setItem יכול לזרוק כשהמכסה מלאה או כשהדפדפן חוסם אחסון, ו“לא זרק“ עדיין אינו ”נשמר“. לכן כל כתיבה נקראת מיד לאחריה, ורק הערך שחזר קובע את מצב הכפתורים; כשהוא אינו מה שביקשנו, הכפתור נשאר כבוי ומופיעה הודעה מעל הטבלה. בלי זה לחיצה שלא נשמרה הייתה נראית בדיוק כמו לחיצה שנשמרה — עד הרענון הבא, שבו הסימון נעלם בלי הסבר.
האחסון חסום בתקרה של 500 רשומות: דיווח נושר מהשאילתה אחרי 90 יום ואינו חוזר, אבל הסימון שלו נשאר. הגזירה היא לפי חותמת הזמן שבתוך המפתח — הישנים נופלים ראשונים — והסימון שנלחץ זה עתה שורד אותה תמיד. בקצב הנוכחי התקרה לא תיגע בכלום במשך שנים; היא שם כדי שהגבול יהיה קיים.
ההתנהגות נבדקת בדפדפן אמיתי ב-tests/test_admin_mcp_tabs_browser.py — לחיצה, שרידות לרענון, ביטול, דחיקה הדדית, אחסון חסום ותקרת הרשומות. בדיקת שרת רואה כפתור; היא אינה יכולה לראות שהלחיצה נשמרה.
מיון והעתקה
שתי הטבלאות הגדולות — בריאות הכלים ועלות הניווט — ניתנות למיון לפי כל עמודה מספרית או עמודת תאריך. כותרת עמודה כזו היא קישור, והמיון נעשה בשרת.
חשוב
מיון של מה שנטען בלבד הוא מיון שקרי. טבלת הסשנים מציגה NAVIGATION_COST_LIMIT שורות מתוך אוכלוסייה גדולה יותר, ומיון בדפדפן על מה שהוצג היה מחזיר את המקסימום מתוך 50 ומציג אותו כמקסימום — בלי שום סימן על המסך שהשורה שחיפשו לא נטענה כלל.
לכן בקשת מיון מושכת NAVIGATION_SORT_FETCH_LIMIT שורות במקום NAVIGATION_COST_LIMIT, ממיינת על כולן, ורק אז חותכת חזרה לגודל התצוגה. מסלול ברירת המחדל — בלי פרמטרי מיון — לא משתנה בכלל.
התקרה הגדולה אינה הבטחה שמשכנו את הכל, והקוד אינו מתייחס אליה ככזו: holds_every_row משווה את מספר השורות שביד מול hasMore ומול הספירה שהשאילתה עצמה גוזרת ב-count() OVER (). כשהן אינן מתלכדות — מכל סיבה, כולל תקרה של PostHog שאיננו מכירים — העמוד מציג תווית גלויה ליד הכותרת במקום להציג מיון חלקי כמיון מלא.
תאים ריקים יורדים לסוף בשני כיווני המיון ואינם משתתפים בהשוואה. ריק אינו אפס: סשנים שנרשמו לפני שהאיסוף עלה לאוויר אין להם כוונה שמורה ומצב הקריאה שלהם לא נשמר, ואילו ריק היה ממוין כאפס הם היו קופצים לראש מיון ”הכי מעט קריאות תוכן“ ונראים כמו ממצא.
הערכים מגיעים מ-JSON של PostHog, כלומר מחוץ לתהליך, ולכן sort_value פוסלת כל מה שאינו מספר סופי — כולל bool (ב-פייתון isinstance(True, int) הוא True) ו-NaN, שאינו זורק אלא מתיישב במקום אקראי. תאריכים מומרים לחותמת מספרית, כדי שמחרוזת בלי אזור זמן לא תפיל את ההשוואה כולה.
פרמטרים
פרמטר |
ערך |
|---|---|
|
|
|
עמודה מתוך |
|
עמודה מתוך |
זוג פרמטרים לכל טבלה ולא פרמטר משותף: לשתי הטבלאות יש עמודה בשם errors, ופרמטר אחד היה דו-משמעי. כל ערך שאינו ברשימה הלבנה פשוט אינו מיון — הקישור נבנה בשרת ומשמר גם את מצב הטבלה השנייה, ולכן מיון של טבלה אחת אינו מוחק את המיון של השנייה.
לחיצה חוזרת על אותה עמודה הופכת כיוון, ולחיצה שלישית חוזרת לברירת המחדל; לצד הכותרת מופיע גם קישור איפוס מפורש, כי ”להפוך כיוון“ לבדו אינו מאפשר לבטל מיון.
הערה
עמודת p95/p50 נגזרת בקוד (with_latency_ratio) ואינה מגיעה מ-PostHog. היחס הוא מה שמפריד בין ”איטי תמיד“ לבין ”איטי לפעמים“, ואלה שתי שאלות דיבוג שונות. התא ריק כשאחד הערכים חסר או כש-p50 הוא אפס — חלוקה באפס זורקת, ו“אינסוף“ אינו מספר שאפשר למיין לפיו.
עמודת ”שגיאות“ ממוינת לפי המספר המוחלט ולא לפי האחוז. העמוד עצמו מסביר שהאחוז מטעה על מדגם קטן, ומיון הוא דירוג — כלומר בדיוק השימוש שבו ההטעיה הזו הכי יקרה.
העתקה
לכל אחת משתי הטבלאות יש כפתור העתקה שמייצר טבלת Markdown עם שורת כותרת, כי היעד הוא הדבקה לצ’אט עם סוכן. שלושה כללים מייצרים אותה:
מעתיקים את מה שרואים. השורות נקראות מה-DOM לפי סדר התצוגה, ולכן ההעתקה מכבדת את המיון ואת כל סינון שיתווסף בעתיד בלי שורת קוד נוספת. ”העתק הכל“ הוא כל השורות המוצגות, והמספר כתוב על הכפתור.
הערכים מ-``data-v`` ולא מהטקסט המוצג. שם יושב המספר בלי
ms, והכוונה המלאה ולא החתוכה ב-64 תווים. היחידה עוברת לשם העמודה דרךdata-copy-head.רשימת האפשרויות נבנית מהשורות שיש (הכל / 25 / 10 / 5), ואופציה שאינה קטנה ממספר השורות אינה נוצרת. אין רשימה קשיחה שצריך לסנכרן ביום שמספר הכלים משתנה.
בסוף ההעתקה נוספת שורת הסייג מהכותרת התחתונה, מאותה מחרוזת שמוצגת בעמוד — בלעדיה מי שמדביק מספרי סשנים לצ’אט מאבד בדיוק את ההסתייגות שהעמוד נבנה כדי לשמר.
בטבלת ”כלים חסרים“ יש כפתור אייקון לכל שורה, שמעתיק את מה שהסוכן ביקש כטקסט גולמי.
אזהרה
navigator.clipboard.writeText זמין ב-secure context בלבד ונדחה ב-NotAllowedError כשההרשאה חסרה. אין נפילה אחורה ל-``document.execCommand``: מסלול חלופי שמצליח לפעמים ומעתיק משהו אחר ממה שביקשו הוא בדיוק סוג הכשל שאי אפשר לאבחן אחר כך. נכשל ← הכפתור אומר זאת.
הפקד עצמו נשלח מהשרת hidden ונחשף ב-JS, כי בלי JS אין העתקה וכפתור מת גרוע מכפתור שאינו שם.
משתני סביבה
שלושה, בשירות הוובאפ בלבד. הרשימה המלאה: משתני סביבה - רפרנס.
משתנה |
הערה |
|---|---|
|
מפתח אישי ( |
|
מזהה הפרויקט המספרי, לבניית נתיב ה-API. ספרות בלבד — ערך אחר נדחה, ראו ההערה למטה |
|
כתובת קריאת הנתונים: |
אזהרה
POSTHOG_HOST נושא ערכים שונים בשני השירותים, ואי אפשר להעתיק אותו
ביניהם. בשירות ה-MCP זו כתובת שליחת האירועים (us.i.posthog.com); כאן זו
כתובת קריאת הנתונים (us.posthog.com). אלה שני מארחים נפרדים אצל PostHog,
ולא שם נרדף.
כתובת השליחה מחזירה 404 על נתיב ה-API — ו-404 פירושו בדרך כלל
”האנדפוינט אינו קיים“, כלומר הודעה ששולחת לחפש בדיוק במקום הלא נכון. לכן
העמוד בודק את הערך לפני ששולח בקשה ואומר במפורש שזו כתובת שליחה. מאותה
סיבה אין כאן ברירת מחדל: משתנה חסר הוא שגיאת קונפיגורציה מוצהרת, ולא נפילה
שקטה לערך של שירות אחר.
בטבלת ה-Config Inspector הערך המוצג הוא של הוובאפ בלבד, כי הכלי קורא את
os.environ של התהליך שלו.
אזהרה
POSTHOG_PROJECT_ID הוא הערך היחיד מבין השלושה שנכנס ל**נתיב הכתובת**
שנשלחת. http_sync רושם את הכתובת המלאה כמאפיין http.url על ה-span,
ולכן ערך שנחת שם בטעות — למשל מפתח שהודבק במשתנה הלא נכון — היה נכתב לערוץ
תצפית. המפתח עצמו לעולם אינו מגיע לכתובת: הוא עובר בכותרת Authorization
בלבד.
לכן הערך נבדק מול רשימה לבנה — ספרות ASCII בלבד — ונדחה כ-config_invalid
לפני שנשלחת בקשה. רשימה לבנה ולא שחורה: היא מצהירה על הצורה הנתמכת במקום
לנסות לזהות ”מה נראה כמו סוד“, וכל ניסיון לזהות סודות מפספס את הצורה שלא
נחזתה מראש. ההודעה, כמו כל הודעות הקונפיגורציה כאן, אינה מצטטת את הערך.
str.isdigit() אינו מספיק לבדיקה הזו: הוא מחזיר True גם על ספרות
יוניקוד שאינן ASCII ('١٢٣', '²').
מצבי כשל
השירות (services/mcp_analytics_service.py) לעולם אינו זורק; הוא מדווח
כשל בשדה error_code. כל אנדפוינט מקבל את המצב שלו בנפרד, ולכן אנדפוינט אחד
שנכשל אינו מחשיך את שני הטאבים האחרים.
|
מתי |
מה לעשות |
|---|---|---|
|
אחד משלושת המשתנים ריק |
ההודעה נוקבת בשם המשתנה החסר |
|
|
ההודעה נוקבת בשם המשתנה ובצורה הנדרשת, ואינה מצטטת את הערך |
|
|
להחליף ל- |
|
|
המפתח פג, בוטל, או חסר לו |
|
|
האנדפוינט אינו קיים בפרויקט. לבדוק קודם את ה-host |
|
|
השאילתה ב-PostHog. ההודעה נגזרת מה- |
|
|
זמני. לרענן בעוד רגע |
|
התשובה אינה במבנה הצפוי |
ראו את ההערה למטה |
הערה
bad_payload אינו קוסמטי. חוזה ה-API מבטיח את results בלבד; columns
אינו מובטח בו, ולכן חיבור שלהם אינו יכול להיות zip תמים — zip על
אורכים שונים חותך בשקט ומייצר שורה שחסרה בה עמודה, בלי שאיש ישים לב. אי-התאמה
מדווחת ככשל מפורש.
מאותה משפחה: קוד סטטוס 200 אינו ראיה להצלחה. error בגוף התשובה הוא
ערוץ כשל נפרד, וקורא שבודק רק את הסטטוס יציג שגיאה כטבלה ריקה.
תקציב זמן
העמוד קורא לכל האנדפוינטים במקביל — DASHBOARD_ENDPOINTS היא הרשימה,
וגם גודל ה-pool נגזר ממנה — אבל המקבול מקצר את הסכום ולא את המקרה הגרוע:
קריאה שנתקעת נתקעת באותה מידה גם כשאחרות רצות לצידה. לכן יש
שלוש הגנות נפרדות — timeout לבקשה בודדת, max_attempts ללולאת ה-retry,
ותקציב עליון שנאכף על ההמתנה עצמה וחותך בוודאות.
שתי מלכודות שהופכות את שתי ההגנות הראשונות לחלשות ממה שהן נראות, ושתיהן נמדדו:
``timeout`` סקלרי אינו תקרה אחת אלא שתיים. requests ממיר ערך יחיד
ל-connect ו-read נפרדים, ו-urllib3 מקבל total=None — כלומר אין
שום חסם על הסכום. timeout=3.0 מתיר עד שש שניות לבקשה, לא שלוש. לכן הערך כאן
הוא טאפל מפורש: connect קצר, כי החיבור ל-PostHog מהיר או שהוא נכשל, ו-read
נדיב יותר, כי שם רצה השאילתה.
``max_attempts`` לבדו אינו מספר הבקשות. ה-Session של http_sync מרכיב על
ה-adapter urllib3.Retry משלו, ושתי השכבות מוכפלות: max_attempts=2 מייצר
עד שש בקשות רשת. לכן הקריאה כאן מעבירה גם adapter_retries=False, שמבטל את
השכבה הפנימית. ראו Resilience לשירותים חיצוניים.
מצב |
חישוב |
תוצאה |
|---|---|---|
קריאה אחת שצורכת את מלוא הזמן |
|
6.0 — נכנס |
המקרה הגרוע המלא, עם ניסיון חוזר |
|
מעל התקציב |
הערה
המספר המדויק של המקרה הגרוע תלוי במדיניות ה-Retry שנקראת ממשתני סביבה:
עם ברירות המחדל הוא 12.75 שניות, ובסביבת ה-CI — שמאפסת את ה-backoff
ואת ה-jitter כדי שהטסטים ירוצו מהר — הוא 12.0. מה שנכון בכל סביבה
הוא היחס, וזה מה שהבדיקה אוכפת: המקרה הגרוע חורג מהתקציב, וקריאה
יחידה מלאה נכנסת מתחתיו.
הערה
ו-``read`` אינו תקרה על כל התשובה. הוא חל על כל קריאת socket בנפרד, ולכן שרת שמטפטף בייטים לאט יכול למשוך בקשה הרבה מעבר לערך שנקבע, בלי שאף אחד מהשניים ייגמר. זו סיבה נוספת לכך שהחישוב כאן הוא הנמקה בלבד: מה שחוסם בפועל הוא התקציב העליון, שנאכף על ההמתנה עצמה ולא על השקע.
וזו הצהרה מכוונת, לא פער שהוסתר. התקציב מבטיח שקריאה יחידה שלוקחת את מלוא
הזמן אינה נחתכת; ניסיון חוזר כן עשוי להיחתך. זה הגיוני — ניסיון חוזר עוזר
כש-503 חוזר מהר, ולא כש-PostHog לוקח שש שניות לענות, ובמקרה כזה הניסיון
השני לא היה עוזר ממילא. להכניס את המקרה הגרוע מתחת לתקציב היה מחייב
connect + read <= 3.125 — כלומר לחנוק שאילתה על 30 יום נתונים.
הקבועים יושבים בראש services/mcp_analytics_service.py. שתי הטענות נאכפות
בבדיקה ולא בהערה: שקריאה מלאה אחת נכנסת, ושהמספר בטבלה למעלה נגזר מהמדיניות
האמיתית ב-resilience.py ולא מועתק כקבוע.
הערה
שני הניסיונות נשמרים בכוונה: התיעוד של PostHog מגדיר את 503 query_capacity
כשגיאה שכדאי לנסות אחריה שוב. לעומת זאת 400 — כולל query_timeout —
אינו מנסה שוב, כי שאילתה שחרגה מהזמן תחרוג ממנו גם בפעם הבאה. ההבחנה מגיעה
משכבת ה-HTTP המשותפת (http_sync) ולא מקוד ייעודי.
אבטחה
המפתח אינו מגיע לדפדפן. השליפה נעשית בצד השרת והתבנית מקבלת שורות מוכנות.
המפתח אינו נכנס לכתובת. הוא עובר ב-
Authorization: Bearer, וה-limitנכנס לגוף ה-JSON — כך שלבקשה אין שורת שאילתה כלל. זה חשוב מפני ש-sentry-sdkרושם את שורת השאילתה של כל בקשה יוצאת, גם מוצלחת, ובקשה בלי שאילתה לא נושאת מה לרשום. ראו Sentry.תוכן שסוכן כתב מרונדר כטקסט.
capabilityבטאב השלישי ו-intentבטור הכוונה הם שניהם טקסט חופשי שסוכן חיצוני חיבר. שניהם מרונדרים כטקסט בלבד — בלי|safeובלי הזרקה ל-DOM מ-JavaScript, וזה נכון גם לתוכן וגם לתכונת ה-titleשנושאת את הטקסט המלא. ”רק אדמינים רואים“ אינו הגנה; אדמינים הם היעד (הכלל המלא:bugbot-rules/xss-innerhtml.mdבריפוamir-bug-patterns, לא בריפו הזה). נבדק גם ב-DOM המרונדר וגם בדפדפן אמיתי, כי מה שקובע הוא מה שהדפדפן בונה ולא מה שהמחרוזת מכילה.הקישורים היוצאים נבנים מקונפיגורציה מאומתת בלבד.
POSTHOG_HOSTו-POSTHOG_PROJECT_IDעוברים את אותה ולידציה שמגנה על הבקשה עצמה, ואם אחד מהם פסול לא נבנה קישור כלל. המפתח אינו משתתף בבנייה — כתובת שנפתחת בדפדפן היא בדיוק המקום שבו סוד לא יכול להיות, כי הוא נכתב להיסטוריה, ל-Refererולכל ערוץ תצפית שרושם כתובות.פרטי הזדהות ב-``POSTHOG_HOST`` נדחים, כולל שם משתמש ריק. הכתובת
https://:secret@us.posthog.comמוחזרת מ-urlsplitעםusername=''— מחרוזת ריקה, כלומר falsy — ולכן בדיקה על הערך בלבד אישרה אותה, והסיסמה נחתה בקישורים שמרונדרים לעמוד. הבדיקה היאis not Noneעלusernameוגם עלpassword, ויש עליה טסט רגרסיה.
איך העמוד מציג את ההודעה, ואיך קוראים אותה
ההודעה מנוקה, ולא נחתכת. הודעת ValidationError של Pydantic היא ארבע
שורות, ושלוש מהן רעש בהקשר של טבלה: כותרת (1 validation error for
get_fileArguments) שנושאת את שם הכלי — שכבר יש לו עמודה משלו; הזחות; ושורת
For further information visit https://errors.pydantic.dev/... שזהה בכל
שגיאה מאותו סוג. מה שנשאר הוא שורה אחת לכל שדה שנדחה:
lines.1 · Input should be a valid integer [input_value='9', input_type=str]
אזהרה
שני מקרים שבהם הניקוי מחק בעבר מידע במקום רעש, ושניהם מכוסים בבדיקות:
הודעה שהגיעה עם \r\n השאירה \r בסוף כל שורה, ואז התבנית של שורת
התיעוד — שנגמרת ב-$ — לא תפסה והקישור שרד בתא; וסוגריים ריקות בתוך
הערך שנדחה (input_value=[]) נמחקו יחד עם ניקוי הסוגריים הריקות שנשארו
מהסרת type=. שניהם אותו שורש: פעולה גורפת על טקסט שמכיל גם רעש וגם ערך.
הפירסור יושב ב-summarize_validation_message ו**נכשל בטוח**: מה שאינו נראה
כמו הודעת Pydantic מוחזר כפי שהוא, בלי לגעת. הכלל הזה חשוב יותר מהניקוי
עצמו — מאז שהשער נפתח לכל סוגי השגיאות, ההודעה בתא יכולה להיות של כל ספרייה,
וקיצור שמנחש פורמט היה מוחק דווקא את מה שאי אפשר לאבחן בלעדיו. המבנה שהפירסור
נשען עליו נמדד על pydantic 2.12.3 ולא נלקח מהתיעוד: הרינדור עצמו הוא
ב-Rust ב-pydantic_core, ואין קוד פייתון לקרוא.
הערה
כיווניות. ההודעה והכוונה הן טקסט טכני באנגלית בתוך עמוד RTL, ולכן שני
התאים נושאים direction: ltr עם unicode-bidi: isolate. בלי זה
האלגוריתם הדו-כיווני של הדפדפן מזיז סוגריים וסימני שוויון לקצה הלא נכון,
ו-[input_value='9'] מוצג שבור. זו תכונה של הטקסט ולא של העימוד, ולכן
היא יושבת על התא ולא על הטבלה — סדר העמודות נשאר RTL.
הכוונה נפתחת בלחיצה. הטקסט מקוצץ בתא, והתא עצמו הוא <button> אמיתי —
לא span עם tabindex. כך Enter ו-Space עובדים בלי JavaScript משלנו,
וקורא מסך מכריז ”כפתור“ במקום להשמיע טקסט שלא ברור שאפשר ללחוץ עליו. הלחיצה
פותחת חלון עם הטקסט המלא.
החלון הוא <dialog> נייטיב שנפתח ב-showModal(), ולא div שמוצג
ומוסתר ביד. ההבדל אינו סגנוני: הדפדפן נועל את הפוקוס בתוך הדיאלוג, סוגר
ב-Escape, מחזיר את הפוקוס לכפתור שפתח, ומצייר את הרקע המעומעם ב-::backdrop
— ארבעה דברים שהגרסה הידנית הייתה צריכה לכתוב, ושלושה מהם היא לא כתבה. הבדיקה
נמדדה בכרומיום אמיתי: Shift+Tab מכפתור הסגירה מתחלף בין הכפתור לדיאלוג
עצמו, ואינו מגיע לתוכן שמאחור.
אזהרה
הטקסט בחלון נכתב ב-textContent, לא ב-innerHTML. זה מסלול שני
לאותו טקסט שסוכן חיצוני כתב: ה-escape של Jinja מגן על הטבלה, והחלון נבנה
ב-JavaScript ולכן צריך הגנה משלו. שני המסלולים נבדקים בנפרד בדפדפן אמיתי,
ויש בדיקה שאוסרת innerHTML בסקריפט של העמוד.
”X שגיאות חדשות מאז הביקור האחרון“
הטבלה מציגה חלון נע של 30 יום, ולכן המספר הכולל יורד מעצמו כששגיאות ישנות נושרות — גם כשנוספו חדשות. כלומר הוא אינו יכול לשמש איתות.
מה שכן יציב הוא חותמת הזמן של השגיאה החדשה ביותר שכבר נראתה. הבאנר סופר כמה
שורות חדשות ממנה, ורק אז שומר את החדשה הנוכחית — הסדר הזה הוא שמונע מהביקור
הנוכחי לאפס את עצמו. הערך נשמר ב-localStorage של הדפדפן.
מצב |
מה קורה |
|---|---|
ביקור ראשון (אין ערך שמור) |
הבאנר שותק. הכול היה ”חדש“, ומספר כזה אינו אומר דבר. הביקור כן נרשם, אחרת גם הבא אחריו היה ”ראשון“ |
|
הבאנר פשוט לא מופיע. הקריאה והכתיבה עטופות, וכשל שלהן אינו נוגע בשאר העמוד — זו נוחות, לא נתון |
ההשוואה |
על |
הטבלה נחתכה בתקרה, וכל השורות שהוצגו חדשות |
הניסוח הופך ל“לפחות X“. הבאנר סופר שורות ב-DOM, והשאילתה מוגבלת
ל- |
הערה
הערך הוא לכל דפדפן בנפרד, לא לכל משתמש. זו החלטה מודעת: הספירה היא עזר תצוגה, והיא לא שווה אוסף חדש במסד או מסלול כתיבה נוסף. מאותה סיבה אין ספירה אמיתית בצד השרת כשהטבלה נחתכת — היא הייתה דורשת לשלוח לשרת את חותמת הזמן ששמורה בדפדפן ולקבל ספירה בחזרה, בקשה נוספת עבור שורת טקסט.
הטורים שנוספו, ומה הם לא אומרים
אאוטליין מול קריאת תוכן. עד עכשיו הייתה עמודה אחת, file_reads, שספרה
את שתי הצורות יחד — בזמן שהן הפוכות במשמעות: אאוטליין הוא המפה (הפתרון),
וקריאת תוכן היא הניווט היקר (הבעיה). עמודה שסופרת את שתיהן יחד לא יכולה
להראות שהאחת החליפה את השנייה, וזה בדיוק המדד שהעמוד נבנה כדי למדוד.
ההבחנה מגיעה מ-ck_read_mode, מאפיין שהשרת מחשב בעצמו — ראו
מצב הקריאה (ck_read_mode). היא אינה מגיעה מהפרמטרים של הקריאה: אלה חסומים
בשער הפרטיות ונשארים חסומים.
אזהרה
אין להשוות בין סשנים בלי לדעת מה הייתה המשימה. 15 קריאות במשימה גדולה יכולות להיות יעילות יותר מ-20 במשימה קטנה. הטור מודד עלות, לא איכות. ההערה הזו מופיעה גם בתחתית הטבלה עצמה, ולא רק כאן.
טור הכוונה. מציג את המשפט שהסוכן כתב — לא סיכום ולא עיבוד, הטקסט עצמו,
מקוצץ בתצוגה עם המלא ב-title. במקום מזהה סשן אטום רואים מה הסוכן ניסה
לעשות.
הערה
שני הדברים האלה ריקים לכל מה שנאסף לפני שהם עלו לאוויר, וזה צפוי.
בסשן ישן אין כוונה שמורה (התא ריק), ולא נשמר מצב קריאה — ולכן כל קריאות
הקובץ שלו נספרות כקריאת תוכן. עמודת ”התחיל“ אומרת לאילו שורות זה נוגע.
באותו אופן, קריאה שנכשלה לפני שההודעות נשמרו מוצגת עם — בעמודת ההודעה:
הצינזור קרה בזמן השליחה ואינו הפיך. תא ריק אינו כשל — כשל מוצג
כהודעה אדומה, וההבחנה הזו נבדקת.
מה שאי אפשר להביא לכאן, ולמה
סיכומי כוונה לסשן ו**אשכולות כוונות** הם כלי API של PostHog ולא
שאילתות: אשכול כוונות הוא עבודת LLM שרצה ונשמרת אצלם, ודורשת הרצת חישוב.
העמוד הזה מדבר עם PostHog רק דרך אנדפוינטים שמורים, שהם HogQL בלבד, ולכן אין
דרך להביא אותם — וחיקוי חלקי היה מציג מספר שנראה כמו discovery rate בלי
להיות אחד.
במקום זה, קישור יוצא. בתחתית טאב עלות הניווט יש שני קישורים לתצוגות
המתאימות ב-PostHog. הם שווים בערכם ואף יותר: האשכולות נותנים discovery rate
מול קטלוג הכלים, והתאמה בין תיאור הכלי לכוונה — כלומר האם התיאור של הכלי גורם
לסוכנים למצוא אותו. פשוט לא כאן.
עיצוב
כל הצבעים במסך הם טוקנים (--card-bg, --glass, --text-primary וכו«) ולא
ערכי HEX, לפי מערכת ערכות הנושא והטוקנים החדשה. לטוקנים הסמנטיים יש fallback לטוקן
גלובלי, ולכן העמוד קריא גם בערכות שאינן מגדירות את כולם. אין בו בלוק override
לכל ערכה — וזה בדיוק מה שקיבוע צבעים היה מחייב.
טסטים
tests/test_mcp_analytics_service.py— השירות, מצבי הכשל והתקציב.tests/test_admin_mcp_page.py— העמוד, דרך בקשת HTTP ומול ה-DOM המרונדר.tests/test_admin_mcp_tabs_browser.py— אינטראקציה בדפדפן אמיתי: מעבר טאבים, מצב ריק, גלישה אופקית בנייד, גלילת הטבלה הרחבה בתוך הכרטיס שלה,titleעל כוונה מקוצצת, וטקסט שנראה כמו תגית שנשאר צומת טקסט ב-DOM. מדולג כשאין Chromium.tests/test_http_sync_adapter_retries.py— שכבת ה-Retry הכפולה שהעמוד הזה חשף, כולל בדיקה שהמסלול הקיים לא זז.
בדיקות העמוד עוברות דרך אותו ממשק שהמשתמש עובר בו ולא קוראות לפונקציות ישירות.
ראו גם
מדידת שימוש (PostHog MCP Analytics) — צד הכתיבה: מה השרת שולח ומה נחסם בשער הפרטיות
שרת ה-MCP — חיבור Claude ל-CodeKeeper — שרת ה-MCP במלואו
Config Inspector (סקירת משתני סביבה) — כלי אדמין מקביל למשתני סביבה