Add hand-logged Visibility row: Profile Appearances and Profile Views
LinkedIn has no file export for either metric (confirmed - no Download option exists on the "Who's viewed your profile" / "Profile appearances" pages, unlike the content analytics file team_exports.py parses), so this is a hand-maintained log (visibility_tracker.py, same "add an entry, no code change needed" pattern as conversation_tracker.py) rather than another auto-parsed file. Shown as its own card above the main dashboard, independent of whether a person's Aggregate Analytics export exists yet - visibility logging and the Monday export are two separate data sources that can each arrive on their own schedule. Seeded with Mckinley's real first week (Oct 20-26: 10 all appearances +67%, 1 search appearance +0%). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ed6e6c457
commit
833cfc6388
2 files changed
+78
-2
No files matched your search
@@ -18,6 +18,7 @@ import engagement_audit
|
|||||||
import theme_tracker
|
import theme_tracker
|
||||||
import conversation_tracker
|
import conversation_tracker
|
||||||
import team_exports
|
import team_exports
|
||||||
|
import visibility_tracker
|
||||||
|
|
||||||
# Overridable via env var so the same code runs unmodified in Docker/internal deployments —
|
# Overridable via env var so the same code runs unmodified in Docker/internal deployments —
|
||||||
# defaults to this machine's path for local dev.
|
# defaults to this machine's path for local dev.
|
||||||
@@ -2698,6 +2699,38 @@ def _team_kpi_row(tiles):
|
|||||||
style={"display": "flex", "gap": "0.75rem", "marginBottom": "1rem"})
|
style={"display": "flex", "gap": "0.75rem", "marginBottom": "1rem"})
|
||||||
|
|
||||||
|
|
||||||
|
def _team_visibility_row(name):
|
||||||
|
"""Profile Appearances/Views - hand-logged (visibility_tracker.py), not parsed
|
||||||
|
from the Aggregate Analytics file, since LinkedIn has no export for either.
|
||||||
|
Shown regardless of whether the person's main export exists yet - visibility
|
||||||
|
logging and the Monday export are two independent data sources."""
|
||||||
|
entry = visibility_tracker.latest_for(name)
|
||||||
|
if not entry:
|
||||||
|
tiles = [
|
||||||
|
kpi("All Appearances", "—", "not logged yet"),
|
||||||
|
kpi("Search Appearances", "—", "not logged yet"),
|
||||||
|
kpi("Profile Views", "—", "not logged yet"),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
week_label = f"week of {entry['week_start']} – {entry['week_end']}"
|
||||||
|
|
||||||
|
def _pct_note(pct):
|
||||||
|
if pct is None:
|
||||||
|
return week_label
|
||||||
|
arrow = "▲" if pct > 0 else ("▼" if pct < 0 else "–")
|
||||||
|
return f"{arrow} {abs(pct)}% past 7 days · {week_label}"
|
||||||
|
|
||||||
|
tiles = [
|
||||||
|
kpi("All Appearances", f"{entry['all_appearances']:,}",
|
||||||
|
_pct_note(entry.get("all_appearances_change_pct"))),
|
||||||
|
kpi("Search Appearances", f"{entry['search_appearances']:,}",
|
||||||
|
_pct_note(entry.get("search_appearances_change_pct")), ORANGE),
|
||||||
|
kpi("Profile Views", f"{entry['profile_views']:,}" if entry.get("profile_views") is not None else "—",
|
||||||
|
week_label if entry.get("profile_views") is not None else "not logged yet", TEAL),
|
||||||
|
]
|
||||||
|
return section("Visibility (hand-logged, no LinkedIn export exists for this)", _team_kpi_row(tiles))
|
||||||
|
|
||||||
|
|
||||||
def _team_engagement_rate_stats(top_posts_df):
|
def _team_engagement_rate_stats(top_posts_df):
|
||||||
"""(best_pct, avg_pct, post_count) - engagement rate per post is Engagements /
|
"""(best_pct, avg_pct, post_count) - engagement rate per post is Engagements /
|
||||||
Impressions, the same definition as the Page's own Avg. Eng. Rate tile. None for
|
Impressions, the same definition as the Page's own Avg. Eng. Rate tile. None for
|
||||||
@@ -2943,10 +2976,11 @@ def _render_team_tab(name):
|
|||||||
data lands. No code change needed when a person's first export arrives -
|
data lands. No code change needed when a person's first export arrives -
|
||||||
team_exports.load_person_data() picks it up automatically by filename. The
|
team_exports.load_person_data() picks it up automatically by filename. The
|
||||||
Download Report button is disabled whenever there's nothing real to send yet."""
|
Download Report button is disabled whenever there's nothing real to send yet."""
|
||||||
|
visibility_row = _team_visibility_row(name)
|
||||||
data = team_exports.load_person_data(name, DATA_DIR, UPLOAD_DIR)
|
data = team_exports.load_person_data(name, DATA_DIR, UPLOAD_DIR)
|
||||||
if not data:
|
if not data:
|
||||||
return _build_empty_person_dashboard(name), True
|
return html.Div([visibility_row, _build_empty_person_dashboard(name)]), True
|
||||||
return _build_person_dashboard(name, data), False
|
return html.Div([visibility_row, _build_person_dashboard(name, data)]), False
|
||||||
|
|
||||||
|
|
||||||
@app.callback(
|
@app.callback(
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Hand-logged Profile Views / Profile Appearances per team member, since LinkedIn
|
||||||
|
doesn't offer a file export for either (unlike the Aggregate Analytics content file
|
||||||
|
team_exports.py parses) - both only show live on a person's own profile page, under
|
||||||
|
"Profile appearances" and "Who's viewed your profile."
|
||||||
|
|
||||||
|
Profile Appearances = how often someone's name/headline showed up across LinkedIn
|
||||||
|
(search results, others' feeds, comments, People You May Know), split into All
|
||||||
|
appearances and the Search-appearances subset. Profile Views = actual visits to the
|
||||||
|
profile page. Add an entry here each Monday alongside that person's export - same
|
||||||
|
"add an entry, no code change needed" pattern as conversation_tracker.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# {
|
||||||
|
# "name": "Mckinley Black",
|
||||||
|
# "week_start": "2026-10-20",
|
||||||
|
# "week_end": "2026-10-26",
|
||||||
|
# "all_appearances": 10,
|
||||||
|
# "all_appearances_change_pct": 67,
|
||||||
|
# "search_appearances": 1,
|
||||||
|
# "search_appearances_change_pct": 0,
|
||||||
|
# "profile_views": None,
|
||||||
|
# },
|
||||||
|
VISIBILITY_LOG = [
|
||||||
|
{
|
||||||
|
"name": "Mckinley Black",
|
||||||
|
"week_start": "2026-10-20",
|
||||||
|
"week_end": "2026-10-26",
|
||||||
|
"all_appearances": 10,
|
||||||
|
"all_appearances_change_pct": 67,
|
||||||
|
"search_appearances": 1,
|
||||||
|
"search_appearances_change_pct": 0,
|
||||||
|
"profile_views": None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def latest_for(name):
|
||||||
|
"""Most recent logged week for a person, by week_end - None if nothing logged yet."""
|
||||||
|
entries = [e for e in VISIBILITY_LOG if e["name"] == name]
|
||||||
|
if not entries:
|
||||||
|
return None
|
||||||
|
return max(entries, key=lambda e: e["week_end"])
|
||||||
Reference in new issue
Block a user