Communitygithub.com

constancetanahrefs/webinar-analyzer

A portable skill spec for building a platform-neutral Webinar Analyzer dashboard: anonymisation gate, column mapping, per-webinar and cross-webinar analytics.

Qu'est-ce que webinar-analyzer ?

webinar-analyzer is a Claude Code agent skill that a portable skill spec for building a platform-neutral Webinar Analyzer dashboard: anonymisation gate, column mapping, per-webinar and cross-webinar analytics.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add constancetanahrefs/webinar-analyzer

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Webinar Analyzer

A tool that turns raw webinar exports into two kinds of answers:

  1. Per webinar — who came, how long they stayed, when they left, who they were, what they said.
  2. Across all webinars — is attendance growing, which slot and format works, who keeps coming back, and what praise and complaints recur.

This skill is stack-neutral. It specifies data contracts, validation rules, computations and chart semantics. Implement it in whatever web stack you have (Flask/Django/FastAPI + Postgres + any charting lib; Streamlit; a notebook; a BI tool). The reference implementation these instructions were extracted from is a Flask + PostgreSQL + ApexCharts app.


Before you build anything: the intake conversation

Do not assume Zoom. Do not assume YouTube. The single biggest failure mode of this tool is hard-coding one platform's export format. The user may run webinars on Teams, GoToWebinar, Livestorm, Demio, Webex, Hopin, Riverside, StreamYard, Crowdcast, BigMarker or a custom stack, and may host replays on Vimeo, Wistia, their own CDN, or nowhere at all.

Run this sequence before writing code:

Step 1 — Ask which platforms are in play

Ask, as a short multiple-choice question:

  • Which platform do you run live webinars on? (Zoom Webinars / Zoom Meetings / Microsoft Teams / GoToWebinar / Livestorm / Demio / Webex / Hopin / BigMarker / Crowdcast / other)
  • Where do post-webinar surveys live? (built into the webinar platform / Typeform / Google Forms / SurveyMonkey / Hubspot / none)
  • Where do replays live, if anywhere? (YouTube / Vimeo / Wistia / self-hosted / gated landing page / no replays)
  • Do you have registration data separate from attendance data? (one combined export / two separate exports / registration only / attendance only)

Step 2 — Ask for one real export, before designing anything

Say plainly: "Upload one real export from each source — attendee, survey, replay — with the personal data already removed. I'll read the actual column headers instead of guessing, and tell you which parts of the dashboard your data can and cannot support."

Insist on this. A single real file removes every assumption. If the user cannot share real data, ask them to paste the header row plus 2–3 fully fabricated rows.

Step 3 — Run the privacy gate on the uploaded file

See Privacy gate. Reject the file if it contains unscrubbed personal data. Do not proceed to mapping until a clean file is provided.

Step 4 — Map their columns to the canonical schema

Show the user an explicit mapping table (their column → canonical field → what it unlocks), and name what is missing. See Field requirements and references/platform-exports.md.

Step 5 — Tell them which charts they get

Print a feature availability report before building:

✅ Available with your data
   Watch-time distribution, concurrent-viewer timeline, drop detection,
   attendance funnel, month-on-month growth, new vs returning.

⚠️  Partially available
   Demographics — you have Country but no Job Title, so the seniority
   donut will be hidden.

❌ Not available
   Replay analytics — no replay export. The Replays tab will be hidden
   until you add one (or you can enter figures manually via the CSV template).

Never build a chart whose input data does not exist. Hide the whole panel and show a one-line explanation of what data would turn it on.


Privacy gate (MANDATORY)

Every uploaded file passes a scrubbing check before parsing, before storage, before analysis. If personal data is found, reject the entire file, name the offending columns and row numbers, and explain how to scrub it. Never partially import. Never auto-anonymise silently.

What counts as personal data

CategoryDetection
Email addresses[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,} in any cell
Phone numbers(?:\+?\d[\s\-().]{0,2}){7,15} with ≥7 digits, excluding pure timestamps/IDs
Postal addressesCell matching street-type keywords (`street
Postcodes / ZIP\b\d{5}(-\d{4})?\b or [A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2} when the column name suggests location
IP addressesIPv4/IPv6 literal
Government / payment IDs\b\d{3}-\d{2}-\d{4}\b (SSN-shaped), \b(?:\d[ -]*?){13,16}\b passing a Luhn check
Suspicious column namesHeader matching `email

Rules

  1. Header scan first. Any column whose name matches the suspicious list is flagged even if the sampled cells look empty — an empty column today can be populated tomorrow.
  2. Scan every cell of every row. Not a sample. Files are small (thousands of rows); a full scan is cheap and a sampled scan gives false assurance.
  3. Free-text survey answers are scanned too. People type "email me at [email protected]" into feedback boxes. This is the most commonly missed leak.
  4. Reject on first category found, but report all of them. Complete the scan and present every problem at once so the user fixes the file in one pass.
  5. Never write a rejected file to disk or database. Hold it in memory, reject, discard.
  6. Log the rejection without the offending values. Log column names and row numbers only — never the detected email/phone itself.

Allowed identifiers (these are NOT rejections)

  • A stable pseudonymous person ID — a hash, UUID or sequential integer that is consistent across files. This is required for cross-webinar analysis (see below).
  • Country / region names, organisation names, job titles, seniority bands.
  • Timestamps, durations, webinar IDs, topic strings.

Rejection message template

❌ Upload rejected — personal data detected in attendees_march.csv

  Column "Email"           → 412 email addresses (rows 2–413)
  Column "Contact Number"  → 88 phone numbers  (rows 2, 5, 9, … )
  Column "What can we improve?" → 3 email addresses inside free text
                                  (rows 47, 112, 288)

Nothing was imported and the file was discarded.

To fix:
  1. Replace the Email column with a stable pseudonymous ID — e.g. a
     SHA-256 hash of the lowercased email, truncated to 16 characters.
     Use the SAME hashing method for every file so the same person is
     recognisable across webinars.
  2. Delete the Contact Number column entirely — it is never used.
  3. Redact the email addresses inside the free-text answers, keeping
     the rest of the comment.

Re-upload once scrubbed. See references/anonymisation.md for a ready-made
scrubbing script.

Why a stable pseudonymous ID is non-negotiable

Half this tool's value is cross-webinar: new vs returning attendees, retention cohorts, repeat registrants, show-up rate per person. All of that needs to recognise the same person across files without knowing who they are. Tell the user this explicitly at intake — if they scrub emails to blanks or random values per file, every cross-webinar chart silently becomes meaningless.

Recommended: person_id = sha256(lower(strip(email)) + fixed_salt)[:16], with the salt stored outside the dataset. references/anonymisation.md contains a drop-in script.


Field requirements

The dashboard runs on a canonical schema. The user's export columns are mapped onto it. Anything the mapping cannot fill is a feature that gets hidden.

Canonical entity: webinar (one row per session)

FieldRequired?TypeUnlocksTypical source column
external_idRequiredtext, uniqueEverything — the join key across attendee/survey/replay filesWebinar ID, Meeting ID, Event ID, Session ID
topicRequiredtextAll labelling; campaign grouping of rerunsTopic, Title, Event name
start_timeRequiredtimestampEvery time-based chart; month-on-month; hour-of-day; the concurrency timelineActual Start Time, Start, Date
duration_minutesStrongly recommendedintConcurrency timeline x-axis; % of session watchedActual Duration, Duration
num_registrantsRecommendedintFunnel, show-up rate (can be derived by counting rows)# Registrants, Registered
unique_viewersRecommendedintHour-of-day chart; headline reach (derivable)Unique Viewers, Attendees
max_concurrentOptionalintPeak-audience statMax Concurrent Views

Timezone rule: decide once whether stored timestamps are UTC or a fixed local zone, write it in the schema comment, and never convert ad hoc. Month bucketing reads straight off this field. Provide a manual month-override mechanism (see Month overrides) for sessions that run just after local midnight for an audience in another timezone.

Canonical entity: attendee_session (one row per person per join)

A person who drops and rejoins produces multiple rows. Never dedupe at ingest — dedupe at analysis time, deliberately.

FieldRequired?TypeUnlocksTypical source column
webinar_idRequiredFKEverything
person_idRequired (pseudonymous)textNew vs returning, retention, repeat registrants, per-person dedupeHashed Email, Attendee ID
attendedRequiredboolAttended/no-show split, funnel, show-up rateAttended (Yes/No)
is_registrantRecommendedboolNo-show cohort; funnelderived from Registration Time being present
join_timeRecommendedtimestampConcurrency timeline, drop detectionJoin Time
leave_timeRecommendedtimestampConcurrency timeline, drop detectionLeave Time
minutesRecommendedintWatch-time histogram, avg watch timeTime in Session (minutes)
country / regionOptionaltextGeography donut, regional timing analysisCountry/Region
organizationOptionaltextAccount-level viewOrganization, Company
job_titleOptionaltextSeniority donut (derived)Job Title, Title, Role
source_nameOptionaltextAcquisition-channel donutSource Name, UTM source, Referrer
registration_timeOptionaltimestampSignup-curve analysisRegistration Time
custom_answersOptionalJSONOne donut per registration questionAny unrecognised column

Unrecognised-column rule: any column in the attendee file that is not one of the known fields above is treated as a registration survey question and stored in custom_answers as {question: answer}. Each such question automatically renders its own donut chart. This is what makes the tool work on platforms you have never seen.

Derived field — seniority: bucket job_title by keyword into C-Level / Founder (ceo, cto, cfo, cmo, coo, chief, founder, owner, president), VP / Director (vp, vice president, head of, director), Manager / Lead (manager, lead, principal), Senior IC (senior, sr.), Junior (junior, jr., intern, associate, assistant), else Individual Contributor. Hide the seniority chart entirely when no row has a job title.

Canonical entity: feedback_response (one row per survey submission)

FieldRequired?TypeUnlocksTypical source column
webinar_idRequiredFKJoining feedback to the sessionMeeting/Webinar ID
person_idOptional (pseudonymous)textLinking feedback to attendance behaviourHashed Email Address
submitted_atOptionaltimestampOrdering, recencySubmitted Date and Time
scoreRecommendedint 1–5Satisfaction distribution + averages"How useful did you find…"
liked_textRecommendedtextPraise theme clustering"What did you like…"
improve_textRecommendedtextConcern theme clustering"What can we do to improve…"
other_answersOptionalJSONEverything else, verbatimremaining question columns

Column detection: find the score / liked / improve columns by keyword match on the header (useful/rate/score; like; improve), case-insensitive, first match wins. Show the user the detected mapping and let them correct it — survey question wording differs at every company.

If the survey lives outside the webinar platform (Typeform, Google Forms), it will not carry a webinar ID. Then require the user to either add a hidden field carrying the webinar ID, or pick the target webinar in the UI at upload time.

Canonical entity: replay_stats (one row per webinar)

Replays are the most platform-variable data of all. Support three ingest paths, in this order of preference:

  1. API connector to the replay host (YouTube Data/Analytics API, Vimeo, Wistia) — only if the user has one and grants credentials.
  2. Native analytics CSV export from the replay host, mapped to the canonical fields.
  3. Manual template CSV — always available, always the fallback. Ship a downloadable template with a header row, a comment block explaining each metric, and one example row.
FieldRequired?TypeUnlocks
webinar_idRequiredjoin keyEverything replay-related
video_id / video_urlOptionaltextDeep-link out to the hosted video; API sync
views_day1OptionalintEarly-demand chart
views_day7OptionalintSustained-demand chart
total_view_hoursOptionalnumericTotal replay consumption chart
avg_view_duration_secOptionalintReplay engagement chart
avg_view_percentageOptionalnumericReplay retention chart
likes, comments, shares, subscribers_gainedOptionalintReaction stats

Every replay field is optional. Store NULL for blanks, never 0 — an unmeasured metric and a measured zero are different facts and the chart must not conflate them. On upsert, COALESCE so a partial re-upload never wipes previously supplied values.


Data model summary

webinars ──┬── attendee_sessions   (many per webinar; many per person per webinar)
           ├── feedback_responses  (many per webinar)
           │      └── feedback_clusters   (LLM: per-webinar themes)
           │             └── theme_groups / theme_members (LLM: cross-webinar themes)
           ├── replay_stats        (0..1 per webinar)
           ├── month_overrides     (0..1 per webinar)
           └── notes               (standalone; decisions & review reminders)

Use a real database (PostgreSQL recommended), not files or in-memory state. Full DDL: references/schema.sql.

Stub webinars: files may arrive in any order — the survey before the attendee report, or replay stats for a webinar not yet imported. When a file references an unknown external_id, create a stub webinar row with just the ID and a placeholder topic, then let the later attendee upload fill it in. Flag stubs prominently in the UI (missing: attendee report, survey, replay stats) and exclude undated stubs from every aggregate, reporting the exclusion visibly rather than silently.

Re-upload semantics: re-uploading a file for an existing webinar replaces that webinar's rows for that data type (delete-then-insert), never appends. Otherwise a double upload silently doubles the attendance.


Ingest pipeline

upload → privacy gate → type detection → parse → map → validate → store → (async) LLM analysis
  1. Accept multiple files at once. Users have a folder per webinar.
  2. Auto-detect file type by sniffing structure — section markers, header signatures — not by filename. Return attendee | survey | replay | unknown. Reject unknown with a message showing the headers found and the headers expected.
  3. Process attendee files before survey files within a batch, so surveys enrich a real webinar rather than creating a stub.
  4. Long work goes async. LLM clustering exceeds any reasonable HTTP timeout. Insert a job row, run in a background worker/thread, and poll from the UI.

Per-webinar analysis

Every chart below states: what it shows, what data it needs, how it is computed, and what decision it informs. Hide any chart whose required fields are absent.

1. Overview stat strip

  • Shows: registrants, unique viewers, attendance rate, max concurrent, duration, average watch time.
  • Needs: webinar meta + attendee rows.
  • Computed: attendance rate = attended people ÷ registrants. Average watch time = mean of per-person summed minutes.
  • Answers: did this session over- or under-perform at a glance.

2. Watch-time distribution (histogram)

  • Shows: how many attendees watched 0–10 min, 10–20 min, and so on, with median and mean marked.
  • Needs: minutes and attended.
  • Computed: sum minutes per person first (rejoins are separate rows — summing per row inflates attendee count and deflates duration), then bin into 10-minute buckets.
  • Answers: is the audience bimodal — a wave of tourists who leave in 5 minutes plus a committed core? A left-heavy histogram means the opening isn't landing; a right-heavy one justifies a longer format.

3. Concurrent viewers over time (area chart)

  • Shows: per-minute count of people in the room, from minute 0 to the end.
  • Needs: join_time, leave_time, webinar start_time, duration_minutes.
  • Computed: for each attendee, mark every minute between join_time − start_time and leave_time − start_time as present; sum across attendees per minute.
  • Answers: where exactly the audience walks out. Cross-referenced against the run sheet, it tells you which segment lost them.

4. Rapid viewership drops (table + shaded regions on chart 3)

  • Shows: ranked time ranges where the audience fell off fastest, with before/after counts, % of peak lost, and clock times.
  • Needs: the same fields as chart 3.
  • Computed: slide over the timeline; at each minute i, look ahead up to 5 minutes for the window with the biggest drop. Flag any window losing more than 10% of the peak concurrency. Jump past a flagged segment so regions never overlap. Sort by absolute drop.
  • Answers: converts a wiggly line into a specific accusation — "we lost 14% of the room between 18:22 and 18:25, which is when the demo started".
  • Tuning: the 10%-of-peak threshold and 5-minute window are the defaults; expose them if sessions vary a lot in size.

5. Demographics: attended vs did not attend (paired donuts)

  • Shows: side-by-side breakdowns of the attended cohort and the no-show cohort by country/region, acquisition source, and seniority.
  • Needs: country / source_name / job_title; attended and is_registrant to split cohorts.
  • Computed: dedupe by person_id first (one vote per human, preferring the row that carries registration detail), then count. Top 8 categories plus an "Other" slice.
  • Answers: who signs up and doesn't show. If one acquisition source over-indexes among no-shows, that channel is delivering low-intent registrations. If one region over-indexes, the time slot is wrong for them.

6. Registration-question donuts (one per detected question)

  • Shows: a donut per custom registration question ("What's your role?", "What do you want to learn?").
  • Needs: any unrecognised column in the attendee file (stored in custom_answers).
  • Computed: dedupe by person, count answers, top 12 + Other.
  • Answers: whether the audience matches the intended ICP, and what they came for — direct input to the next session's outline.

7. Satisfaction score distribution (bar)

  • Shows: count of responses per score 1–5, plus the average.
  • Needs: score.
  • Computed: plain count per value; average weighted by count.
  • Answers: the headline quality number. Treat with caution — response counts are typically 6–40 per webinar, so read the distribution shape, not the third decimal place.
  • Warning to display: show n next to every average. A 4.67 from 6 responses is not better than a 4.07 from 40.

8. Feedback themes — per webinar (LLM clustering)

  • Shows: the "what did you like" and "what can we improve" free text, broken into atomic points and grouped into named themes, each expandable to the verbatim quotes behind it.
  • Needs: liked_text / improve_text.
  • Computed: one LLM call per question. Prompt instructions that matter:
    • Split each response into atomic points — one idea per point, so a response covering three things counts in three themes.
    • Cluster points that express the same underlying idea; name each cluster in 3–6 words with a one-sentence summary.
    • Return strict JSON, no prose or code fences.
    • Each response may be used at most once across all clusters — and enforce this in code afterwards regardless of what the model returns, first cluster wins (clusters are largest-first, so the strongest theme keeps the response).
    • Skip non-answers (n/a, ., -, nothing, none).
  • Answers: turns 40 free-text comments into 6 named issues with evidence attached.

9. Polarity correction (critical, and usually forgotten)

  • Shows: each theme labelled by what it actually expressespraise, concern or none — not by which survey box it was typed into.
  • Needs: the clustered themes.
  • Computed: a cheap LLM classification pass over each cluster label + summary. Split the UI by polarity, not by source question. Count and display how many clusters were "misfiled" relative to their source box.
  • Why it matters: in real data roughly 16% of "what can we improve" answers are people saying nothing needs improving, and complaints occasionally land in the "what did you like" box. Counting the improve box as complaints overstates your problem rate by a sixth. This single correction is the difference between a trustworthy dashboard and a misleading one.

10. Replay performance (per webinar)

  • Shows: day-1 views, day-7 views, total view-hours, average view duration and retention % for this session's replay.
  • Needs: a replay_stats row.
  • Computed: direct display; avg_view_duration_sec ÷ 60 for a minutes read-out.
  • Answers: whether the recording out-earns the live event — often it does, which changes how much you invest in promoting the replay.
  • When absent: show a single panel explaining the three ingest paths and offering the template download. Do not render empty axes.

Cross-webinar analysis

Organise as an executive Summary page plus six drill-down tabs. Render each tab as its own page request, not hidden divs — most charting libraries measure their container at construction time and a chart built inside display:none renders at zero width and never recovers.

Summary page (the landing view)

Headline numbers only, each linking to the detail view that explains it:

  • Month scoreboard — total attendance for the selected month with a MoM delta, unique attendees, registrations, show-up rate, webinars run. Defaults to the latest month; ?month=YYYY-MM scopes the page. An unknown value falls back to the latest rather than erroring, so a stale bookmark still renders.
  • Best and least attended webinar of the scope — ranked on unique attendees, not satisfaction score. Show the score alongside, never as the ranking key (see the small-n warning in chart 7).
  • Top recurring praise and criticism — the consolidated cross-webinar themes, top 4 of each polarity. Feedback themes may be read all-time even while the rest of the page is month-scoped; one month rarely contains enough webinars to judge recurrence. Make the scope toggle visible.
  • Data health strip — how many webinars are undated, missing attendee data, missing survey data, missing replay data. Coverage must be visible, or every number above is quietly wrong.

Tab 1 — Growth (month on month)

Vocabulary — define these in the UI, they are constantly conflated:

TermDefinition
attendanceattended (webinar, person) pairs. One person at two webinars in a month counts twice. This is the headline.
unique_attendeesdistinct people who attended at least once that month
registrations(webinar, person) signup pairs
no_showsregistrations that never attended
net_newpeople attending for the very first time ever, in this month
returningattendees who had attended some earlier webinar
repeat_factorattendance ÷ unique_attendees — how many sessions the average attendee sat through

Chart 1.1 — Attendance per month (line + rolling average)

  • Shows: total attendance per month with a 3-month trailing mean overlaid.
  • Computed: count attendance pairs per month; rolling mean over a 3-month window.
  • Answers: are we growing? The rolling line exists so one unusually big webinar doesn't read as a trend. Always show both.

Chart 1.2 — Where the growth comes from (stacked line)

  • Shows: attendance decomposed two ways — webinars run × average attendees per webinar, and net-new vs returning.
  • Computed: per month, units, avg_per_unit, net_new, returning.
  • Answers: distinguishes "we ran fewer sessions" from "our audience is shrinking" — completely different problems with the same symptom.

Chart 1.3 — Registration → attendance funnel (100% stacked bar)

  • Shows: each month's registrations split into attended vs no-show.
  • Computed: showup_rate = attendance ÷ registrations.
  • Answers: whether promotion or the event itself is the bottleneck. Falling registrations = a marketing problem. Falling show-up rate = a reminder-email, timing or expectation problem.

Chart 1.4 — Month detail table

  • Every metric per month with MoM deltas. Render rate changes as 25.3% → 30.9%, not +5.6pp — the jargon loses people.

Session vs campaign toggle Same-topic reruns (a session run twice for different timezones) can be viewed per session or grouped as one campaign, with a person attending both sessions counted once in campaign mode. Derive the campaign key by normalising the topic — strip bracketed suffixes, (Session 2), - APAC, Rerun, punctuation and case. A campaign belongs to the month its earliest session ran.

Month overrides Provide a manual override assigning a webinar to a different month, always with a required reason, always listed in the UI. Needed when a session starts just after local midnight for an audience in another timezone — the audience attended the previous day in their own zone, so the raw month misrepresents when they showed up. Never apply such a correction silently.

Tab 2 — Timing & format

Chart 2.1 — Average unique viewers by start hour (bar)

  • Shows: for each hour of day a webinar has been run, the average unique viewers and the number of webinars behind that average.
  • Computed: bucket webinars by start_time.hour; average unique_viewers.
  • Answers: which slot draws the biggest audience.
  • Mandatory caveat in the UI: show the sample size on every bar. A single 17:00 webinar is not evidence about 17:00.
  • Enhancement when country data exists: split by attendee region (APAC / EMEA / Americas / Other) — a global average across timezones is close to meaningless.

Chart 2.2 — Same content, different slot (paired comparison)

  • Shows: reruns of the same topic side by side, with each session's registrations, attendance and show-up rate.
  • Computed: group by campaign key; only show groups with 2+ sessions.
  • Answers: the cleanest natural experiment available — same content, different time, so the difference really is the slot.

Format tagging Format (panel / solo / demo / AMA / guest) should be a manual tag the user applies. Do not infer it from the title; the guesses are wrong often enough to poison the comparison.

Tab 3 — Engagement

Chart 3.1 — Average live watch time per webinar (bar)

  • Shows: per webinar, mean per-person minutes watched, chronologically.
  • Computed: per webinar, sum minutes per person, then average across people.
  • Answers: which sessions held the room, independent of how many showed up. A big audience that leaves at minute 8 is worse than a small one that stays for 50.

Chart 3.2 — Registrations split per webinar (stacked bar)

  • Shows: three segments per webinar — no-show, new attendee (never attended before), returning attendee.
  • Computed: walk webinars in chronological order maintaining a running set of people who have attended before; classify each attendee against that set, then add them to it. Order matters: process strictly by date.
  • Answers: is each session recycling the same crowd or reaching new people? Sustained growth needs a healthy net-new band.

Chart 3.3 — All-time new vs returning (pie) — the same split aggregated over the whole period.

Table 3.4 — Per-webinar breakdown — sortable table behind the charts, with counts and percentages.

Tab 4 — Wins & fails (cross-webinar theme consolidation)

Per-webinar LLM clustering names the same idea differently every time: "Practical use cases shown", "Practical use cases and strategies" and "Practical actionable use cases" are one theme in three costumes. Consolidating them is what turns feedback into a roadmap.

The consolidation pipeline (background job):

  1. Classify polarity on any unlabelled cluster (see chart 9) — grouping keys off polarity, so this must run first.
  2. Embed every per-webinar cluster's label + summary with a text-embedding model.
  3. Agglomeratively merge on cosine similarity into canonical themes. Threshold ≈ 0.62 as a starting point — tuned so the "practical use cases" family merges without collapsing "pacing" into "session length". Re-tune on the user's own data and say so.
  4. One cheap LLM call to name each canonical theme.
  5. Persist groups and members so the UI reads instantly.

Chart 4.1 — Canonical theme list, split by polarity

  • Shows: each theme with how many webinars it appeared in, how many feedback points it covers, its trajectory, and the member webinars.
  • Recurring threshold: a theme appearing in 3+ webinars is systemic; fewer is an isolated remark. Display the distinction.

Chart 4.2 — Trajectory labels Measured as share of that webinar's feedback points, never raw mentions — response counts swing from 6 to 43 per webinar, so a theme can look like it's fading purely because fewer people replied. Comparison window: last 4 webinars vs everything before. Minimum 3 webinars before a direction is claimed at all. Relative-change threshold ≈ 0.25.

LabelMeaning
Getting louderRaised noticeably more often lately (by share)
Still coming upAbout the same rate as before
Coming up lessRaised noticeably less often lately — encouraging, but survey data can't confirm a fix
Not raised latelyAbsent from the last 4 webinars, having appeared in 3+ before. Strongest available signal, still not proof
Newly raisedOnly appears in the last 4 webinars; too early to tell
IsolatedFewer than 3 webinars; no direction can be read

There is deliberately no "Resolved" label. Survey data can never tell you a problem was fixed — only that people stopped raising it. Every label is phrased as an observation about frequency, not a claim about cause. Preserve this framing; it is the difference between a tool people trust and one that gets them burned in a QBR.

Chart 4.3 — Month-by-month digest

  • Shows: top 3 praised and top 3 criticised themes per month.
  • Answers: whether a fix stuck — did the complaint you addressed in March drop off in April?

Chart 4.4 — Polarity stats panel — how many clusters are praise / concern / none, and how many were misfiled relative to their source question. Makes the correction from chart 9 auditable rather than magic.

Tab 5 — Audience

Table 5.1 — Top registrants and attendance rate

  • Shows: one row per person across all webinars — registrations, attendances, attendance rate, and a context label (job title / organisation / country from their most recent record).
  • Computed: group by person_id; count distinct webinars, not rows, so a rejoin doesn't inflate the count.
  • Answers: your superfans (invite them to speak, ask for testimonials) and your chronic no-shows (a different nurture track, or stop counting them as pipeline).
  • Performance note: default the table to "2+ registrations" and cap the payload (≈750 rows). Shipping every single-registration row bloats the page for data nobody scrolls to — state what was omitted.

Chart 5.2 — Retention cohort grid

  • Shows: of the people who first attended in month X, how many returned in each later month.
  • Computed: first-attendance month per person; then per later month, count of that cohort present.
  • Answers: whether the webinar programme builds an audience or churns through one.

Tab 6 — Replays

Charts 6.1–6.4: replay views per webinar (day 1 and day 7, grouped bar), average view duration per webinar, total view-hours per webinar, average view percentage / retention per webinar.

  • Needs: replay_stats rows.
  • Answers: which topics have a long tail. Replay-heavy topics deserve evergreen promotion and are candidates for a gated asset; live-heavy topics are event-driven and should be scheduled for their live moment.
  • Coverage panel: always show how many webinars have replay data linked vs total, plus the three ingest paths and the template download. Partial coverage must be visible, or the chart reads as "these webinars had no replay views" when it means "we don't have the numbers".

Notes & decisions panel

A small standing feature that pays for itself: pinned notes attached to the dashboard, each with a kind (decision / info / risk), optional markdown body, and an optional review date that raises a banner when it arrives, with snooze options.

Use it for exactly the things that get forgotten: "we chose CSV upload for replay stats until the API access is sorted — revisit 5 Nov", "webinar 22 is reassigned to July, here's why". Seed known decisions on first run with overwrite=False so a restart never clobbers the user's edits.


Implementation guidance

Anti-patterns to avoid

Don'tDo
Hard-code one platform's export formatDetect type, map columns, report what's missing
Sum minutes across rowsSum per person first — rejoins are separate rows
Count feedback themes by which box they came fromClassify polarity independently
Rank webinars by satisfaction scoreRank by unique attendees; show score with its n
Show raw theme mention counts over timeUse share of feedback points
Silently drop undated/stub webinars from aggregatesExclude them and say so on screen
Store 0 for an unmeasured replay metricStore NULL; COALESCE on upsert
Append on re-uploadDelete-then-insert per webinar per data type
Build charts in hidden tabsOne request per tab
Run the LLM pass synchronouslyBackground job + polling
Auto-anonymise a file containing PIIReject it, name the columns, tell them how to fix it
Infer webinar format from the titleManual tags only

Build order

Ship in slices; each is independently useful.

  1. Intake conversation + privacy gate + column mapper + attendee ingest + the webinar list with data-health flags.
  2. Per-webinar page: stat strip, watch histogram, concurrency timeline, drop detection, demographics.
  3. Survey ingest, score distribution, per-webinar LLM clustering, polarity correction.
  4. Cross-webinar: growth, timing, engagement, audience.
  5. Cross-webinar theme consolidation (embeddings + merge + naming), trajectories, monthly digest.
  6. Replays: template CSV, manual ingest, optional API connector.
  7. Summary page, pulling headlines from all of the above.
  8. Notes & review reminders.

Tooltip discipline

Every metric, threshold and derived label carries an inline explainer in the UI stating what it measures and what it excludes. Any new control, metric or threshold ships with its tooltip in the same change — never after. Users cannot audit a number they can't define, and a dashboard whose numbers can't be audited stops being used.


References

  • references/platform-exports.md — export formats and column names for Zoom, Teams, GoToWebinar, Livestorm, Demio, Webex, YouTube, Vimeo, Wistia, and how to handle an unknown platform.
  • references/anonymisation.md — the PII detector and a ready-to-run scrubbing script the user can apply before upload.
  • references/schema.sql — canonical PostgreSQL DDL for every table.
  • references/llm-prompts.md — verbatim prompts for clustering, polarity classification and theme naming.
  • references/chart-catalog.md — one-line summary of every chart: inputs, computation, required fields, hide-when condition.

Skills associés