Where we start
The analyst track works on one real public dataset all the way through: GH Archive, the hourly record of GitHub's public activity. It is not a product you run, which is the point. It has the shape of every product's event stream, with bots, renames, a few actors doing most of the work and a signup moment it never records, and nothing about it has been cleaned up for teaching. Eight sessions take it from one hour of raw JSON to a bench that tests activation definitions on 8,348 real new repositories.
★ What you walk out with tonight. A tracking plan as a Python dictionary, a validator that maps and type-checks every event in a real 279,130-event hour, the three counts it prints, and the rule that people are keyed on an id and never on a login.
A plan that can fail
6 min liveWritten as code, a tracking plan has three parts: the names (Object Action, as leader session 1 argues), the rule that says which raw event becomes which plan event, and the properties each plan event must carry, with a type. Everything that arrives either maps to a plan event, maps to nothing, or maps and breaks a property. Those three buckets are the whole output of a validator.
LiveThe plan as a dictionary3 min▶
Each plan event has a when rule and a props map. The rule names the raw event type and, where one raw type hides several moments, the payload key and value that picks the right one: a CreateEvent is only "Repository Created" when ref_type == "repository", a PullRequestEvent is only "Pull Request Opened" when action == "opened". The props map is name to Python type. Nothing else. A plan this small can be read in a code review, which is most of its value.
Keeping the plan in the repository next to the code that sends events means a change to one shows up in the diff of the other. Commercial tools offer the same idea as a hosted schema with a "block unplanned events" switch; the principle is identical and the code version costs nothing.
Self-studyWhy "not in the plan" is a count, not an error2 min▶
89,490 events in the hour map to no plan event: 29,113 branch and tag creations, 15,033 deletions, 13,398 issue comments, 9,662 pull request events that are not openings, 8,515 reviews and the long tail. Printing that count, by type, every run, is what stops a plan from rotting. When the count for a type jumps, something new is being sent, and either the plan grows a row or someone asks why the product started emitting it.
The event nobody planned. A mobile release adds a "screen viewed" call to every screen for a debugging session and nobody removes it. Event volume triples, the analytics bill follows, and the weekly active chart jumps because a user who only opened the app now counts as active. A validator that prints "not in the plan: 4,000,000 events of a type we have never seen" on the morning after the release is the cheapest possible alarm.
Valid by type, wrong by meaning
5 min liveA type check is the floor, not the ceiling. On the real hour every tracked event carries every required property with the right type, and the validator prints zero. Then one extra line, a check on what a value means rather than what type it is, finds 234 "Code Pushed" events that carry no commits.
LiveThree kinds of property check3 min▶
Presence: is the property there at all. Type: is it an int, a string, a bool. Meaning: is it a value that fits what the event claims happened, a push with at least one commit, a price above zero, a timestamp not in the future. Most tools check the first two. The third is where product definitions live, and it has to be written by someone who knows what the event is for.
Keep meaning checks as warnings with counts, not as filters that silently drop rows. The 234 are real events. Whether they count toward "active" is a definition question for session 3, and the validator's job is to make sure the question gets asked.
Self-studyMissing is not the same as false2 min▶
GitHub adds an org object only when a repository belongs to an organisation, so "is_org" is derived from whether the key exists. That is a legitimate encoding, and a dangerous one to copy: in your own product, a property that is absent because the client failed to send it looks exactly like one absent because it does not apply. Plans should say, per property, whether absent means false or means unknown.
Which field is the person
4 min liveGitHub's stream is the easy case: every event is by a logged-in account. Even so, 7 account ids appear with more than one login in a single hour, and no login appears with more than one id. Most products have the harder case too: a visitor who is anonymous until they sign up, and whose earlier events have to be joined to the account they become.
LiveKey on the id, join the labels2 min▶
The rule is short: the person is a stable id; logins, emails and display names are properties of the person that can change. The build-along counts ids per login and logins per id on the real hour and finds 7 and 0. The asymmetry is the lesson: an id never splits, a label sometimes does.
For anonymous-then-known products, PostHog's documentation describes the standard mechanism: every visitor gets an anonymous id, and calling identify at login "merges the anonymous person into the identified person", so the pre-signup events join the account. Two warnings from the same docs matter to an analyst: call reset() on logout so a shared device does not merge two people, and never identify with a generic value like "null" or "guest", which PostHog blocks because it would merge strangers.
Self-studyWhat merging does to your history2 min▶
A merge rewrites the past: yesterday's "anonymous visitor who left" becomes today's "customer who browsed first". Funnels computed before and after a merge disagree for the same day, and both are correct for their moment. Freeze the definition of the population you report on, and when a number changes after the fact, check whether identity resolution moved people before assuming behaviour moved.
Validate one real hour against the plan
★ 22 min · everyone buildsStandard-library Python and one GH Archive file. Every number printed below is what this code prints on the file 2024-03-04-15.json.gz. The file is about 150 MB and the full pass takes under a minute on a laptop.
Download the hour. GH Archive names files by UTC date and hour, no login needed. One hour is enough for everything tonight.
Write the plan. Names, the rule that picks the raw moment, and typed properties. This is the whole contract.
Map a raw event to a plan event. Return the plan name and only the properties the plan asks for, or None.
Stream the file and count the three buckets. One pass, line by line, so memory stays flat. The meaning check for pushes and the identity check ride along.
Print the report. These are the numbers the real hour gives.
Run it on a schedule, not once. The report is most useful as a diff: yesterday's counts against today's. A new raw type appearing in the untracked list, or a violation count leaving zero, is the moment to look, and the plan is what makes either one visible.
Before session 2
30 min- Add a sixth plan event for "Repository Forked" (raw type ForkEvent) with the properties you think it needs, re-run, and write down the new tracked share.
- Write one more meaning check, for example a pull request whose base branch is empty, and count it on the hour.
- Print the 7 renamed ids' event counts. Decide, in one sentence, whether a weekly active count by login would have over- or under-counted them, and by how many.
- Keep the file. Session 2 loads the same hour into pandas and looks at who produced it.
Three questions
Clear all three and analyst session 1 gets stamped in your passport.
1 · The validator prints zero type violations on the real hour. What does that prove?
Presence and type are the floor. Meaning checks, written by someone who knows what the event is for, are where definitions are protected, and they should report counts rather than silently drop rows.
2 · Why does the plan map CreateEvent to "Repository Created" only when ref_type == "repository"?
The rule that picks the raw moment is as much a part of the plan as the name. When you inherit a stream, look for names that hide several moments and split them in the mapping.
3 · In a product with anonymous visitors, what does calling reset() on logout protect against?
Identify merges the anonymous person into the known one; reset ends that link. Without it, two people on one laptop become one person in every chart, which is a merge nobody can undo from the data alone.
What this session teaches, and where it came from
Every fact on this page, and its verification tier, is recorded in the course's source map.
Analyst session 1 cheat sheet · pin this
The plan says what the events should be. Session 2 looks at who actually produced them: the same hour in pandas, where half the accounts appear once, one account without a bot suffix produces 25,458 events, and bots write 15.5 percent of the diary. Then we cut the stream into sessions and meet the tie problem that a timestamp to the second cannot solve.